From c2810ad23b37fe8da2bc54d5212087edafba24a0 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 19 Oct 2020 19:28:38 +0300 Subject: [PATCH 01/14] Create case client --- .../plugins/case/server/client/add_comment.ts | 120 ++++++++++++++ x-pack/plugins/case/server/client/create.ts | 74 +++++++++ x-pack/plugins/case/server/client/index.ts | 22 +++ x-pack/plugins/case/server/client/types.ts | 49 ++++++ x-pack/plugins/case/server/client/update.ts | 150 ++++++++++++++++++ x-pack/plugins/case/server/services/index.ts | 2 +- 6 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/case/server/client/add_comment.ts create mode 100644 x-pack/plugins/case/server/client/create.ts create mode 100644 x-pack/plugins/case/server/client/index.ts create mode 100644 x-pack/plugins/case/server/client/types.ts create mode 100644 x-pack/plugins/case/server/client/update.ts diff --git a/x-pack/plugins/case/server/client/add_comment.ts b/x-pack/plugins/case/server/client/add_comment.ts new file mode 100644 index 0000000000000..e97e889af4c0e --- /dev/null +++ b/x-pack/plugins/case/server/client/add_comment.ts @@ -0,0 +1,120 @@ +/* + * 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 } 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 = ({ + caseService, + userActionService, +}: CaseClientFactoryArguments) => async ({ + request, + savedObjectsClient, + caseId, + comment, +}: CaseClientAddComment) => { + 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/create.ts b/x-pack/plugins/case/server/client/create.ts new file mode 100644 index 0000000000000..1e5039c1dec13 --- /dev/null +++ b/x-pack/plugins/case/server/client/create.ts @@ -0,0 +1,74 @@ +/* + * 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 } 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 = ({ + caseService, + caseConfigureService, + userActionService, +}: CaseClientFactoryArguments) => async ({ + request, + savedObjectsClient, + theCase, +}: CaseClientCreate) => { + 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/index.ts b/x-pack/plugins/case/server/client/index.ts new file mode 100644 index 0000000000000..784a7b5a0ea22 --- /dev/null +++ b/x-pack/plugins/case/server/client/index.ts @@ -0,0 +1,22 @@ +/* + * 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 './create'; +import { update } from './update'; +import { addComment } from './add_comment'; + +export const createCaseClient = ({ + caseConfigureService, + caseService, + userActionService, +}: CaseClientFactoryArguments): CaseClient => { + return { + create: create({ caseConfigureService, caseService, userActionService }), + update: update({ caseConfigureService, caseService, userActionService }), + addComment: addComment({ caseConfigureService, caseService, 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..4b41ecc45792b --- /dev/null +++ b/x-pack/plugins/case/server/client/types.ts @@ -0,0 +1,49 @@ +/* + * 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 } from '../../common/api'; +import { + CaseConfigureServiceSetup, + CaseServiceSetup, + CaseUserActionServiceSetup, +} from '../services'; + +export interface CaseClientFunctionArguments { + request: KibanaRequest; + savedObjectsClient: SavedObjectsClientContract; +} + +export interface CaseClientCreate extends CaseClientFunctionArguments { + theCase: CasePostRequest; +} + +export interface CaseClientUpdate extends CaseClientFunctionArguments { + theCase: CasesPatchRequest; +} + +export interface CaseClientAddComment extends CaseClientFunctionArguments { + caseId: string; + comment: CommentRequest; +} + +export interface CaseClientFactoryArguments { + caseConfigureService: CaseConfigureServiceSetup; + caseService: CaseServiceSetup; + userActionService: CaseUserActionServiceSetup; +} + +export interface CaseClient { + create: (args: CaseClientCreate) => void; + update: (args: CaseClientUpdate) => void; + addComment: (args: CaseClientAddComment) => void; +} + +export interface CaseClientFactoryArguments { + caseConfigureService: CaseConfigureServiceSetup; + caseService: CaseServiceSetup; + userActionService: CaseUserActionServiceSetup; +} diff --git a/x-pack/plugins/case/server/client/update.ts b/x-pack/plugins/case/server/client/update.ts new file mode 100644 index 0000000000000..c12dcd9656e64 --- /dev/null +++ b/x-pack/plugins/case/server/client/update.ts @@ -0,0 +1,150 @@ +/* + * 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, +} 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 = ({ caseService, userActionService }: CaseClientFactoryArguments) => async ({ + request, + savedObjectsClient, + theCase, +}: CaseClientUpdate) => { + const query = pipe( + excess(CasesPatchRequestRt).decode(theCase), + 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/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 { From afb0a738c338a42471ff2f7af749c1a99e1a827c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 19 Oct 2020 20:53:53 +0300 Subject: [PATCH 02/14] Create case context --- x-pack/plugins/case/server/plugin.ts | 57 +++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 9cf045da3e700..df58d69d66e34 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -5,7 +5,7 @@ */ import { first, map } from 'rxjs/operators'; -import { Logger, PluginInitializerContext } from 'kibana/server'; +import { IContextProvider, Logger, PluginInitializerContext, RequestHandler } from 'kibana/server'; import { CoreSetup } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -18,7 +18,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,9 +38,15 @@ export interface PluginsSetup { export class CasePlugin { private readonly log: Logger; + private caseService: CaseService; + private caseConfigureService: CaseConfigureService; + private userActionService: CaseUserActionService; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = this.initializerContext.logger.get(); + this.caseService = new CaseService(this.log); + this.caseConfigureService = new CaseConfigureService(this.log); + this.userActionService = new CaseUserActionService(this.log); } public async setup(core: CoreSetup, plugins: PluginsSetup) { @@ -47,26 +61,27 @@ 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({ + const caseService = await this.caseService.setup({ authentication: plugins.security != null ? plugins.security.authc : null, }); - const caseConfigureService = await caseConfigureServicePlugin.setup(); - const userActionService = await userActionServicePlugin.setup(); + const caseConfigureService = await this.caseConfigureService.setup(); + const userActionService = await this.userActionService.setup(); + + core.http.registerRouteHandlerContext( + 'case', + this.createRouteHandlerContext({ caseService, caseConfigureService, userActionService }) + ); const router = core.http.createRouter(); initCaseApi({ - caseConfigureService, caseService, + caseConfigureService, userActionService, router, }); @@ -79,4 +94,26 @@ export class CasePlugin { public stop() { this.log.debug(`Stopping Case Workflow`); } + + private createRouteHandlerContext = ({ + caseService, + caseConfigureService, + userActionService, + }: { + caseService: CaseServiceSetup; + caseConfigureService: CaseConfigureServiceSetup; + userActionService: CaseUserActionServiceSetup; + }): IContextProvider, 'case'> => { + return async (context, request) => { + return { + getCaseClient: () => { + return createCaseClient({ + caseService, + caseConfigureService, + userActionService, + }); + }, + }; + }; + }; } From 1ebacbaa22c8db1558e971629f7f698b1dec3df8 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 19 Oct 2020 21:16:47 +0300 Subject: [PATCH 03/14] Pass savedObject --- x-pack/plugins/case/server/client/add_comment.ts | 8 ++------ x-pack/plugins/case/server/client/create.ts | 7 ++----- x-pack/plugins/case/server/client/index.ts | 12 +++++++++--- x-pack/plugins/case/server/client/types.ts | 2 +- x-pack/plugins/case/server/client/update.ts | 8 ++++---- x-pack/plugins/case/server/plugin.ts | 6 +++++- 6 files changed, 23 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/case/server/client/add_comment.ts b/x-pack/plugins/case/server/client/add_comment.ts index e97e889af4c0e..69325db64e6e9 100644 --- a/x-pack/plugins/case/server/client/add_comment.ts +++ b/x-pack/plugins/case/server/client/add_comment.ts @@ -18,14 +18,10 @@ import { CaseClientAddComment, CaseClientFactoryArguments } from './types'; import { CASE_SAVED_OBJECT } from '../saved_object_types'; export const addComment = ({ + savedObjectsClient, caseService, userActionService, -}: CaseClientFactoryArguments) => async ({ - request, - savedObjectsClient, - caseId, - comment, -}: CaseClientAddComment) => { +}: CaseClientFactoryArguments) => async ({ request, caseId, comment }: CaseClientAddComment) => { const query = pipe( excess(CommentRequestRt).decode(comment), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/client/create.ts b/x-pack/plugins/case/server/client/create.ts index 1e5039c1dec13..0459b6e43f95c 100644 --- a/x-pack/plugins/case/server/client/create.ts +++ b/x-pack/plugins/case/server/client/create.ts @@ -21,14 +21,11 @@ import { import { CaseClientCreate, CaseClientFactoryArguments } from './types'; export const create = ({ + savedObjectsClient, caseService, caseConfigureService, userActionService, -}: CaseClientFactoryArguments) => async ({ - request, - savedObjectsClient, - theCase, -}: CaseClientCreate) => { +}: CaseClientFactoryArguments) => async ({ request, theCase }: CaseClientCreate) => { const query = pipe( excess(CasePostRequestRt).decode(theCase), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/client/index.ts b/x-pack/plugins/case/server/client/index.ts index 784a7b5a0ea22..e26fa8563e722 100644 --- a/x-pack/plugins/case/server/client/index.ts +++ b/x-pack/plugins/case/server/client/index.ts @@ -10,13 +10,19 @@ import { update } from './update'; import { addComment } from './add_comment'; export const createCaseClient = ({ + savedObjectsClient, caseConfigureService, caseService, userActionService, }: CaseClientFactoryArguments): CaseClient => { return { - create: create({ caseConfigureService, caseService, userActionService }), - update: update({ caseConfigureService, caseService, userActionService }), - addComment: addComment({ caseConfigureService, caseService, userActionService }), + create: create({ savedObjectsClient, caseConfigureService, caseService, userActionService }), + update: update({ savedObjectsClient, caseConfigureService, caseService, userActionService }), + addComment: addComment({ + savedObjectsClient, + caseConfigureService, + caseService, + userActionService, + }), }; }; diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index 4b41ecc45792b..47c5f5bcd74f9 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -14,7 +14,6 @@ import { export interface CaseClientFunctionArguments { request: KibanaRequest; - savedObjectsClient: SavedObjectsClientContract; } export interface CaseClientCreate extends CaseClientFunctionArguments { @@ -31,6 +30,7 @@ export interface CaseClientAddComment extends CaseClientFunctionArguments { } export interface CaseClientFactoryArguments { + savedObjectsClient: SavedObjectsClientContract; caseConfigureService: CaseConfigureServiceSetup; caseService: CaseServiceSetup; userActionService: CaseUserActionServiceSetup; diff --git a/x-pack/plugins/case/server/client/update.ts b/x-pack/plugins/case/server/client/update.ts index c12dcd9656e64..832dfeeaab5c6 100644 --- a/x-pack/plugins/case/server/client/update.ts +++ b/x-pack/plugins/case/server/client/update.ts @@ -24,11 +24,11 @@ import { getCaseToUpdate, transformCaseConnectorToEsConnector } from '../routes/ import { CaseClientUpdate, CaseClientFactoryArguments } from './types'; -export const update = ({ caseService, userActionService }: CaseClientFactoryArguments) => async ({ - request, +export const update = ({ savedObjectsClient, - theCase, -}: CaseClientUpdate) => { + caseService, + userActionService, +}: CaseClientFactoryArguments) => async ({ request, theCase }: CaseClientUpdate) => { const query = pipe( excess(CasesPatchRequestRt).decode(theCase), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index df58d69d66e34..600d6182b7027 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -75,7 +75,7 @@ export class CasePlugin { core.http.registerRouteHandlerContext( 'case', - this.createRouteHandlerContext({ caseService, caseConfigureService, userActionService }) + this.createRouteHandlerContext({ core, caseService, caseConfigureService, userActionService }) ); const router = core.http.createRouter(); @@ -96,18 +96,22 @@ export class CasePlugin { } private createRouteHandlerContext = ({ + core, caseService, caseConfigureService, userActionService, }: { + core: CoreSetup; caseService: CaseServiceSetup; caseConfigureService: CaseConfigureServiceSetup; userActionService: CaseUserActionServiceSetup; }): IContextProvider, 'case'> => { return async (context, request) => { + const [{ savedObjects }] = await core.getStartServices(); return { getCaseClient: () => { return createCaseClient({ + savedObjectsClient: savedObjects.getScopedClient(request), caseService, caseConfigureService, userActionService, From b02cd01ef73ed8fbbd6fd151446fed49ba568f03 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 19 Oct 2020 21:40:41 +0300 Subject: [PATCH 04/14] Use case client on routes --- .../routes/api/cases/comments/post_comment.ts | 117 ++------------ .../server/routes/api/cases/patch_cases.ts | 149 ++---------------- .../case/server/routes/api/cases/post_case.ts | 69 ++------ 3 files changed, 29 insertions(+), 306 deletions(-) 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..d02a9d7da74c8 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,11 @@ */ 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'; -export function initPostCommentApi({ - caseConfigureService, - caseService, - router, - userActionService, -}: RouteDeps) { +export function initPostCommentApi({ router }: RouteDeps) { router.post( { path: CASE_COMMENTS_URL, @@ -34,101 +21,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; + 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: caseClient.addComment({ request, caseId, comment }), }); } catch (error) { return response.customError(wrapError(error)); 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..e513417e7d0ff 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,11 @@ * 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'; -export function initPatchCasesApi({ - caseConfigureService, - caseService, - router, - userActionService, -}: RouteDeps) { +export function initPatchCasesApi({ router }: RouteDeps) { router.patch( { path: CASES_URL, @@ -37,126 +17,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 theCase = request.body; - const updateFilterCases = updateCases.filter((updateCase) => { - const { id, version, ...updateCaseAttributes } = updateCase; - return Object.keys(updateCaseAttributes).length > 0; + try { + return response.ok({ + body: caseClient.update({ request, theCase }), }); - - 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.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.ts index 5d8113b685741..4f87a7a9337ea 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,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 { 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'; -export function initPostCaseApi({ - caseService, - caseConfigureService, - router, - userActionService, -}: RouteDeps) { +export function initPostCaseApi({ router }: RouteDeps) { router.post( { path: CASES_URL, @@ -31,53 +18,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; + try { return response.ok({ - body: CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: newCase, - }) - ), + body: caseClient.create({ request, theCase }), }); } catch (error) { return response.customError(wrapError(error)); From c4292f72042d5abf22bc6c1b74244895c4675e0d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 19 Oct 2020 21:41:13 +0300 Subject: [PATCH 05/14] Use constant --- x-pack/plugins/case/server/plugin.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 600d6182b7027..67f04f614df1d 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -9,6 +9,7 @@ import { IContextProvider, Logger, PluginInitializerContext, RequestHandler } fr import { CoreSetup } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; +import { APP_ID } from '../common/constants'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; @@ -74,7 +75,7 @@ export class CasePlugin { const userActionService = await this.userActionService.setup(); core.http.registerRouteHandlerContext( - 'case', + APP_ID, this.createRouteHandlerContext({ core, caseService, caseConfigureService, userActionService }) ); @@ -105,7 +106,7 @@ export class CasePlugin { caseService: CaseServiceSetup; caseConfigureService: CaseConfigureServiceSetup; userActionService: CaseUserActionServiceSetup; - }): IContextProvider, 'case'> => { + }): IContextProvider, typeof APP_ID> => { return async (context, request) => { const [{ savedObjects }] = await core.getStartServices(); return { From ad59842395732d9d4f842719c8573d9d4cab83f8 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 19 Oct 2020 21:55:32 +0300 Subject: [PATCH 06/14] Improve types of client --- x-pack/plugins/case/server/client/add_comment.ts | 14 ++++++++++++-- x-pack/plugins/case/server/client/create.ts | 13 +++++++++++-- x-pack/plugins/case/server/client/types.ts | 14 ++++++++++---- x-pack/plugins/case/server/client/update.ts | 6 +++++- .../routes/api/cases/comments/post_comment.ts | 2 +- .../case/server/routes/api/cases/patch_cases.ts | 2 +- .../case/server/routes/api/cases/post_case.ts | 2 +- 7 files changed, 41 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/case/server/client/add_comment.ts b/x-pack/plugins/case/server/client/add_comment.ts index 69325db64e6e9..2d81f294f2f71 100644 --- a/x-pack/plugins/case/server/client/add_comment.ts +++ b/x-pack/plugins/case/server/client/add_comment.ts @@ -11,7 +11,13 @@ import { identity } from 'fp-ts/lib/function'; import { flattenCaseSavedObject, transformNewComment } from '../routes/api/utils'; -import { throwErrors, excess, CaseResponseRt, CommentRequestRt } from '../../common/api'; +import { + throwErrors, + excess, + CaseResponseRt, + CommentRequestRt, + CaseResponse, +} from '../../common/api'; import { buildCommentUserActionItem } from '../services/user_actions/helpers'; import { CaseClientAddComment, CaseClientFactoryArguments } from './types'; @@ -21,7 +27,11 @@ export const addComment = ({ savedObjectsClient, caseService, userActionService, -}: CaseClientFactoryArguments) => async ({ request, caseId, comment }: CaseClientAddComment) => { +}: CaseClientFactoryArguments) => async ({ + request, + caseId, + comment, +}: CaseClientAddComment): Promise => { const query = pipe( excess(CommentRequestRt).decode(comment), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/client/create.ts b/x-pack/plugins/case/server/client/create.ts index 0459b6e43f95c..0d237d3319fe9 100644 --- a/x-pack/plugins/case/server/client/create.ts +++ b/x-pack/plugins/case/server/client/create.ts @@ -11,7 +11,13 @@ import { identity } from 'fp-ts/lib/function'; import { flattenCaseSavedObject, transformNewCase } from '../routes/api/utils'; -import { CasePostRequestRt, throwErrors, excess, CaseResponseRt } from '../../common/api'; +import { + CasePostRequestRt, + throwErrors, + excess, + CaseResponseRt, + CaseResponse, +} from '../../common/api'; import { buildCaseUserActionItem } from '../services/user_actions/helpers'; import { getConnectorFromConfiguration, @@ -25,7 +31,10 @@ export const create = ({ caseService, caseConfigureService, userActionService, -}: CaseClientFactoryArguments) => async ({ request, theCase }: CaseClientCreate) => { +}: CaseClientFactoryArguments) => async ({ + request, + theCase, +}: CaseClientCreate): Promise => { const query = pipe( excess(CasePostRequestRt).decode(theCase), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index 47c5f5bcd74f9..d883c5796c400 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -5,7 +5,13 @@ */ import { KibanaRequest, SavedObjectsClientContract } from '../../../../../src/core/server'; -import { CasePostRequest, CasesPatchRequest, CommentRequest } from '../../common/api'; +import { + CasePostRequest, + CasesPatchRequest, + CommentRequest, + CaseResponse, + CasesResponse, +} from '../../common/api'; import { CaseConfigureServiceSetup, CaseServiceSetup, @@ -37,9 +43,9 @@ export interface CaseClientFactoryArguments { } export interface CaseClient { - create: (args: CaseClientCreate) => void; - update: (args: CaseClientUpdate) => void; - addComment: (args: CaseClientAddComment) => void; + create: (args: CaseClientCreate) => Promise; + update: (args: CaseClientUpdate) => Promise; + addComment: (args: CaseClientAddComment) => Promise; } export interface CaseClientFactoryArguments { diff --git a/x-pack/plugins/case/server/client/update.ts b/x-pack/plugins/case/server/client/update.ts index 832dfeeaab5c6..b885fc393e674 100644 --- a/x-pack/plugins/case/server/client/update.ts +++ b/x-pack/plugins/case/server/client/update.ts @@ -18,6 +18,7 @@ import { CasesPatchRequestRt, ESCasePatchRequest, CasePatchRequest, + CasesResponse, } from '../../common/api'; import { buildCaseUserActions } from '../services/user_actions/helpers'; import { getCaseToUpdate, transformCaseConnectorToEsConnector } from '../routes/api/cases/helpers'; @@ -28,7 +29,10 @@ export const update = ({ savedObjectsClient, caseService, userActionService, -}: CaseClientFactoryArguments) => async ({ request, theCase }: CaseClientUpdate) => { +}: CaseClientFactoryArguments) => async ({ + request, + theCase, +}: CaseClientUpdate): Promise => { const query = pipe( excess(CasesPatchRequestRt).decode(theCase), fold(throwErrors(Boom.badRequest), identity) 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 d02a9d7da74c8..98b77d2830091 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 @@ -31,7 +31,7 @@ export function initPostCommentApi({ router }: RouteDeps) { try { return response.ok({ - body: caseClient.addComment({ request, caseId, comment }), + body: await caseClient.addComment({ request, caseId, comment }), }); } catch (error) { return response.customError(wrapError(error)); 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 e513417e7d0ff..17490df2e5bf4 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 @@ -26,7 +26,7 @@ export function initPatchCasesApi({ router }: RouteDeps) { try { return response.ok({ - body: caseClient.update({ request, theCase }), + body: await caseClient.update({ request, theCase }), }); } catch (error) { return response.customError(wrapError(error)); 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 4f87a7a9337ea..6c94f116965d3 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 @@ -26,7 +26,7 @@ export function initPostCaseApi({ router }: RouteDeps) { try { return response.ok({ - body: caseClient.create({ request, theCase }), + body: await caseClient.create({ request, theCase }), }); } catch (error) { return response.customError(wrapError(error)); From 3661f983a06dde3ccde7bb0397b52b6ee838dbb6 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 21 Oct 2020 11:54:51 +0300 Subject: [PATCH 07/14] Return case client from start --- .../case/server/client/__mocks__/index.ts | 98 ++++++ .../plugins/case/server/client/create.test.ts | 164 ++++++++++ .../plugins/case/server/client/index.test.ts | 62 ++++ x-pack/plugins/case/server/client/index.ts | 2 + x-pack/plugins/case/server/client/types.ts | 2 +- .../plugins/case/server/client/update.test.ts | 298 ++++++++++++++++++ x-pack/plugins/case/server/client/update.ts | 4 +- x-pack/plugins/case/server/plugin.ts | 52 ++- .../case/server/services/__mocks__/index.ts | 37 +++ x-pack/plugins/case/server/types.ts | 17 + 10 files changed, 718 insertions(+), 18 deletions(-) create mode 100644 x-pack/plugins/case/server/client/__mocks__/index.ts create mode 100644 x-pack/plugins/case/server/client/create.test.ts create mode 100644 x-pack/plugins/case/server/client/index.test.ts create mode 100644 x-pack/plugins/case/server/client/update.test.ts create mode 100644 x-pack/plugins/case/server/services/__mocks__/index.ts create mode 100644 x-pack/plugins/case/server/types.ts diff --git a/x-pack/plugins/case/server/client/__mocks__/index.ts b/x-pack/plugins/case/server/client/__mocks__/index.ts new file mode 100644 index 0000000000000..6bcadd533dba8 --- /dev/null +++ b/x-pack/plugins/case/server/client/__mocks__/index.ts @@ -0,0 +1,98 @@ +/* + * 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 } from '../../../common/api'; +import { mockCases, mockCaseConfigure } from '../../routes/api/__fixtures__'; + +const createdAt = mockCases[0].attributes.created_at; +const updatedAt = mockCases[0].attributes.updated_at; + +export const elasticUser = mockCases[0].attributes.created_by; + +export const comment = { + comment: 'Solve this fast!', + id: 'comment-1', + createdAt, + createdBy: elasticUser, + pushedAt: null, + pushedBy: null, + updatedAt: null, + updatedBy: null, + version: 'WzQ3LDFc', +}; + +export const tags: string[] = ['defacement']; +export const connector = mockCases[0].attributes.connector; + +export const postCase = { + title: mockCases[0].attributes.title, + description: mockCases[0].attributes.description, + tags, + connector: { ...connector, fields: null }, +}; + +export const patchCases = { + cases: [ + { + id: mockCases[0].id, + title: 'Title updated', + description: 'Description updated', + version: mockCases[0].version ?? 'WzAsMV0=', + }, + ], +}; + +export const patchConnector = { + cases: [ + { + id: mockCases[0].id, + connector: { id: 'jira', name: 'jira', type: ConnectorTypes.jira, fields: null }, + version: mockCases[0].version ?? 'WzAsMV0=', + }, + ], +}; + +export const theCase = { + closedAt: null, + closedBy: null, + id: 'case-1', + comments: [comment], + createdAt, + createdBy: elasticUser, + connector, + description: 'This is a brand new case of a bad meanie defacing data', + externalService: null, + status: 'open', + tags, + title: 'Super Bad Security Issue', + totalComment: 1, + updatedAt, + updatedBy: elasticUser, + version: 'WzQ3LDFd', +}; + +export const casePostResponse = { + ...mockCases[0], + attributes: { + ...mockCases[0].attributes, + updated_at: null, + updated_by: null, + }, +}; + +export const caseConfigureResponse = { + saved_objects: mockCaseConfigure[0], + total: 1, + per_page: 20, + page: 1, +}; + +export const getCasesResponse = { + saved_objects: [mockCases[0]], + total: 1, + per_page: 20, + page: 1, +}; diff --git a/x-pack/plugins/case/server/client/create.test.ts b/x-pack/plugins/case/server/client/create.test.ts new file mode 100644 index 0000000000000..a4d9fec0905c4 --- /dev/null +++ b/x-pack/plugins/case/server/client/create.test.ts @@ -0,0 +1,164 @@ +/* + * 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 { ConnectorTypes } from '../../common/api'; +import { + createCaseServiceMock, + createConfigureServiceMock, + createUserActionServiceMock, +} from '../services/__mocks__'; + +import { create } from './create'; +import { CaseClient } from './types'; +import { elasticUser, casePostResponse, postCase, caseConfigureResponse } from './__mocks__'; + +const caseService = createCaseServiceMock(); +const caseConfigureService = createConfigureServiceMock(); +const userActionService = createUserActionServiceMock(); +const savedObjectsClient = savedObjectsClientMock.create(); +const request = {} as KibanaRequest; + +describe('create()', () => { + let createHandler: CaseClient['create']; + + beforeEach(() => { + jest.resetAllMocks(); + caseService.getUser.mockResolvedValue(elasticUser); + caseConfigureService.find.mockResolvedValue(caseConfigureResponse); + caseService.postNewCase.mockResolvedValue(casePostResponse); + + createHandler = create({ + savedObjectsClient, + caseService, + caseConfigureService, + userActionService, + }); + }); + + describe('happy path', () => { + test('it creates the case correctly', async () => { + const res = await createHandler({ request, theCase: postCase }); + expect(res).toEqual({ + id: 'mock-id-1', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { id: 'none', name: 'none', type: '.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: 'open', + tags: ['defacement'], + updated_at: null, + updated_by: null, + version: 'WzAsMV0=', + }); + }); + + test('it creates the case without connector in the configuration', async () => { + caseConfigureService.find.mockResolvedValue({ ...caseConfigureResponse, saved_objects: [] }); + + const res = await createHandler({ request, theCase: postCase }); + expect(res).toEqual({ + id: 'mock-id-1', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { id: 'none', name: 'none', type: '.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: 'open', + tags: ['defacement'], + updated_at: null, + updated_by: null, + version: 'WzAsMV0=', + }); + }); + }); + + describe('unhappy path', () => { + test('it throws when missing title', async () => { + expect.assertions(1); + caseConfigureService.find.mockResolvedValue({ ...caseConfigureResponse, saved_objects: [] }); + createHandler({ + request, + // @ts-expect-error + theCase: { + description: 'desc', + tags: [], + connector: postCase.connector, + }, + }).catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when missing description', async () => { + expect.assertions(1); + caseConfigureService.find.mockResolvedValue({ ...caseConfigureResponse, saved_objects: [] }); + createHandler({ + request, + // @ts-expect-error + theCase: { + title: 'title', + tags: [], + connector: postCase.connector, + }, + }).catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when missing tags', async () => { + expect.assertions(1); + caseConfigureService.find.mockResolvedValue({ ...caseConfigureResponse, saved_objects: [] }); + createHandler({ + request, + // @ts-expect-error + theCase: { + title: 'title', + description: 'desc', + connector: postCase.connector, + }, + }).catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when missing connector ', async () => { + expect.assertions(1); + caseConfigureService.find.mockResolvedValue({ ...caseConfigureResponse, saved_objects: [] }); + createHandler({ + request, + // @ts-expect-error + theCase: { title: 'a title', description: 'desc', tags: [] }, + }).catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when connector missing the right fields', async () => { + expect.assertions(1); + caseConfigureService.find.mockResolvedValue({ ...caseConfigureResponse, saved_objects: [] }); + createHandler({ + request, + theCase: { + title: 'a title', + description: 'desc', + tags: [], + connector: { + id: 'jira', + name: 'jira', + type: ConnectorTypes.jira, + // @ts-expect-error + fields: {}, + }, + }, + }).catch((e) => expect(e).not.toBeNull()); + }); + }); +}); 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..941f5ca7d9409 --- /dev/null +++ b/x-pack/plugins/case/server/client/index.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { createCaseClient } from '.'; +import { + createCaseServiceMock, + createConfigureServiceMock, + createUserActionServiceMock, +} from '../services/__mocks__'; + +import { create } from './create'; +import { update } from './update'; +import { addComment } from './add_comment'; + +jest.mock('./create'); +jest.mock('./update'); +jest.mock('./add_comment'); + +const caseService = createCaseServiceMock(); +const caseConfigureService = createConfigureServiceMock(); +const userActionService = createUserActionServiceMock(); +const savedObjectsClient = savedObjectsClientMock.create(); + +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, + caseConfigureService, + caseService, + userActionService, + }); + + expect(createMock).toHaveBeenCalledWith({ + savedObjectsClient, + caseConfigureService, + caseService, + userActionService, + }); + + expect(updateMock).toHaveBeenCalledWith({ + savedObjectsClient, + caseConfigureService, + caseService, + userActionService, + }); + + expect(addCommentMock).toHaveBeenCalledWith({ + savedObjectsClient, + caseConfigureService, + caseService, + userActionService, + }); + }); +}); diff --git a/x-pack/plugins/case/server/client/index.ts b/x-pack/plugins/case/server/client/index.ts index e26fa8563e722..8c746c7d053d9 100644 --- a/x-pack/plugins/case/server/client/index.ts +++ b/x-pack/plugins/case/server/client/index.ts @@ -9,6 +9,8 @@ import { create } from './create'; import { update } from './update'; import { addComment } from './add_comment'; +export { CaseClient } from './types'; + export const createCaseClient = ({ savedObjectsClient, caseConfigureService, diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index d883c5796c400..5c6e5066e6d5d 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -27,7 +27,7 @@ export interface CaseClientCreate extends CaseClientFunctionArguments { } export interface CaseClientUpdate extends CaseClientFunctionArguments { - theCase: CasesPatchRequest; + cases: CasesPatchRequest; } export interface CaseClientAddComment extends CaseClientFunctionArguments { diff --git a/x-pack/plugins/case/server/client/update.test.ts b/x-pack/plugins/case/server/client/update.test.ts new file mode 100644 index 0000000000000..eb73a662b72a2 --- /dev/null +++ b/x-pack/plugins/case/server/client/update.test.ts @@ -0,0 +1,298 @@ +/* + * 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 { + createCaseServiceMock, + createConfigureServiceMock, + createUserActionServiceMock, +} from '../services/__mocks__'; +import { mockCases } from '../routes/api/__fixtures__'; + +import { update } from './update'; +import { CaseClient } from './types'; +import { + elasticUser, + patchConnector, + patchCases, + caseConfigureResponse, + getCasesResponse, +} from './__mocks__'; + +const caseService = createCaseServiceMock(); +const caseConfigureService = createConfigureServiceMock(); +const userActionService = createUserActionServiceMock(); +const savedObjectsClient = savedObjectsClientMock.create(); +const request = {} as KibanaRequest; + +describe('update()', () => { + let updateHandler: CaseClient['update']; + beforeEach(() => { + jest.resetAllMocks(); + const { id, version, ...patchCaseAttributes } = patchCases.cases[0]; + caseService.getUser.mockResolvedValue(elasticUser); + caseService.getCases.mockResolvedValue(getCasesResponse); + caseService.patchCases.mockResolvedValue({ + saved_objects: [ + { + ...mockCases[0], + version: 'WzAsMV1=', + attributes: { ...mockCases[0].attributes, ...patchCaseAttributes }, + }, + ], + }); + caseConfigureService.find.mockResolvedValue(caseConfigureResponse); + + updateHandler = update({ + savedObjectsClient, + caseService, + caseConfigureService, + userActionService, + }); + }); + + describe('happy path', () => { + test('it updates the case correctly', async () => { + const res = await updateHandler({ request, cases: patchCases }); + expect(res).toEqual([ + { + id: 'mock-id-1', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { id: 'none', name: 'none', type: '.none', fields: null }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { full_name: 'elastic', email: 'testemail@elastic.co', username: 'elastic' }, + description: 'Description updated', + external_service: null, + title: 'Title updated', + status: 'open', + tags: ['defacement'], + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + version: 'WzAsMV1=', + }, + ]); + }); + + test('it updates the connector correctly', async () => { + const { + id: patchConnectorId, + version: patchConnectorVersion, + ...patchConnectorAttributes + } = patchConnector.cases[0]; + + caseService.patchCases.mockResolvedValue({ + saved_objects: [ + { + ...mockCases[0], + version: 'WzAsMV1=', + attributes: { ...mockCases[0].attributes, ...patchConnectorAttributes }, + }, + ], + }); + + const res = await updateHandler({ request, cases: patchConnector }); + expect(res).toEqual([ + { + id: 'mock-id-1', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { id: 'jira', name: 'jira', type: '.jira', 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: 'open', + tags: ['defacement'], + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + version: 'WzAsMV1=', + }, + ]); + }); + + test('it updates the status correctly: closed', async () => { + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + })); + + await updateHandler({ + request, + cases: { + cases: [ + { + id: 'mock-id-1', + version: 'WzAsMV0=', + status: 'closed', + }, + ], + }, + }); + + expect(caseService.patchCases.mock.calls[0][0].cases[0].updatedAttributes).toEqual({ + status: 'closed', + closed_at: '2019-11-25T21:54:48.952Z', + closed_by: { + email: 'testemail@elastic.co', + full_name: 'elastic', + username: 'elastic', + }, + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + email: 'testemail@elastic.co', + full_name: 'elastic', + username: 'elastic', + }, + }); + }); + + test('it updates the status correctly: open', async () => { + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + })); + + caseService.getCases.mockResolvedValue({ + ...getCasesResponse, + saved_objects: [ + { ...mockCases[0], attributes: { ...mockCases[0].attributes, status: 'closed' } }, + ], + }); + + await updateHandler({ + request, + cases: { + cases: [ + { + id: 'mock-id-1', + version: 'WzAsMV0=', + status: 'open', + }, + ], + }, + }); + + expect(caseService.patchCases.mock.calls[0][0].cases[0].updatedAttributes).toEqual({ + status: 'open', + closed_at: null, + closed_by: null, + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + email: 'testemail@elastic.co', + full_name: 'elastic', + username: 'elastic', + }, + }); + }); + }); + + describe('unhappy path', () => { + test('it throws when missing id', async () => { + expect.assertions(1); + updateHandler({ + request, + cases: { + cases: [ + // @ts-expect-error + { + title: 'update', + }, + ], + }, + }).catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when missing version', async () => { + expect.assertions(1); + updateHandler({ + request, + cases: { + cases: [ + // @ts-expect-error + { + id: '123', + title: 'update', + }, + ], + }, + }).catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when fields are identical', async () => { + expect.assertions(1); + updateHandler({ + request, + cases: { + cases: [ + { + id: 'mock-id-1', + version: 'WzAsMV0=', + title: 'Super Bad Security Issue', + }, + ], + }, + }).catch((e) => + expect(e.message).toBe('All update fields are identical to current version.') + ); + }); + + test('it throws when case does not exist', async () => { + caseService.getCases.mockResolvedValue({ saved_objects: [{ id: 'not-exists', error: {} }] }); + expect.assertions(1); + updateHandler({ + request, + cases: { + cases: [ + { + id: 'not-exists', + version: 'WzAsMV0=', + title: 'Super Bad Security Issue', + }, + ], + }, + }).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); + updateHandler({ + request, + cases: { + cases: [ + { + id: 'mock-id-1', + version: 'WzAsMV1=', + title: 'Super Bad Security Issue', + }, + ], + }, + }).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/update.ts b/x-pack/plugins/case/server/client/update.ts index b885fc393e674..5b943055a8735 100644 --- a/x-pack/plugins/case/server/client/update.ts +++ b/x-pack/plugins/case/server/client/update.ts @@ -31,10 +31,10 @@ export const update = ({ userActionService, }: CaseClientFactoryArguments) => async ({ request, - theCase, + cases, }: CaseClientUpdate): Promise => { const query = pipe( - excess(CasesPatchRequestRt).decode(theCase), + excess(CasesPatchRequestRt).decode(cases), fold(throwErrors(Boom.badRequest), identity) ); diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 67f04f614df1d..a25207fde85b2 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -5,7 +5,13 @@ */ import { first, map } from 'rxjs/operators'; -import { IContextProvider, Logger, PluginInitializerContext, RequestHandler } from 'kibana/server'; +import { + IContextProvider, + KibanaRequest, + Logger, + PluginInitializerContext, + RequestHandler, +} from 'kibana/server'; import { CoreSetup } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -39,15 +45,12 @@ export interface PluginsSetup { export class CasePlugin { private readonly log: Logger; - private caseService: CaseService; - private caseConfigureService: CaseConfigureService; - private userActionService: CaseUserActionService; + private caseService?: CaseServiceSetup; + private caseConfigureService?: CaseConfigureServiceSetup; + private userActionService?: CaseUserActionServiceSetup; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = this.initializerContext.logger.get(); - this.caseService = new CaseService(this.log); - this.caseConfigureService = new CaseConfigureService(this.log); - this.userActionService = new CaseUserActionService(this.log); } public async setup(core: CoreSetup, plugins: PluginsSetup) { @@ -68,28 +71,47 @@ export class CasePlugin { )}] and plugins [${Object.keys(plugins)}]` ); - const caseService = await this.caseService.setup({ + this.caseService = await new CaseService(this.log).setup({ authentication: plugins.security != null ? plugins.security.authc : null, }); - const caseConfigureService = await this.caseConfigureService.setup(); - const userActionService = await this.userActionService.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, caseConfigureService, userActionService }) + this.createRouteHandlerContext({ + core, + caseService: this.caseService, + caseConfigureService: this.caseConfigureService, + userActionService: this.userActionService, + }) ); const router = core.http.createRouter(); initCaseApi({ - caseService, - caseConfigureService, - userActionService, + caseService: this.caseService, + caseConfigureService: this.caseConfigureService, + userActionService: this.userActionService, router, }); } - public start() { + public async start(core: CoreSetup) { this.log.debug(`Starting Case Workflow`); + const [{ savedObjects }] = await core.getStartServices(); + + const getCaseClientWithRequest = async (request: KibanaRequest) => { + return createCaseClient({ + savedObjectsClient: savedObjects.getScopedClient(request), + caseService: this.caseService!, + caseConfigureService: this.caseConfigureService!, + userActionService: this.userActionService!, + }); + }; + + return { + getCaseClientWithRequest, + }; } public stop() { diff --git a/x-pack/plugins/case/server/services/__mocks__/index.ts b/x-pack/plugins/case/server/services/__mocks__/index.ts new file mode 100644 index 0000000000000..f8420cbe4e37d --- /dev/null +++ b/x-pack/plugins/case/server/services/__mocks__/index.ts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +export const createCaseServiceMock = () => ({ + 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 = () => ({ + delete: jest.fn(), + get: jest.fn(), + find: jest.fn(), + patch: jest.fn(), + post: jest.fn(), +}); + +export const createUserActionServiceMock = () => ({ + 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 274c0cc5e0c1817e0ecf7f5d7d577bfb5f83827b Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 21 Oct 2020 18:53:40 +0300 Subject: [PATCH 08/14] Fix types --- .../case/server/routes/api/cases/comments/post_comment.ts | 3 ++- x-pack/plugins/case/server/routes/api/cases/patch_cases.ts | 5 +++-- x-pack/plugins/case/server/routes/api/cases/post_case.ts | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) 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 98b77d2830091..bcf688c9d2478 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 @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { escapeHatch, wrapError } from '../../utils'; import { RouteDeps } from '../../types'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CommentRequest } from '../../../../../common/api'; export function initPostCommentApi({ router }: RouteDeps) { router.post( @@ -27,7 +28,7 @@ export function initPostCommentApi({ router }: RouteDeps) { const caseClient = context.case.getCaseClient(); const caseId = request.params.case_id; - const comment = request.body; + const comment = request.body as CommentRequest; try { return response.ok({ 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 17490df2e5bf4..8e0dd85101cef 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 @@ -7,6 +7,7 @@ import { escapeHatch, wrapError } from '../utils'; import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; +import { CasesPatchRequest } from '../../../../common/api'; export function initPatchCasesApi({ router }: RouteDeps) { router.patch( @@ -22,11 +23,11 @@ export function initPatchCasesApi({ router }: RouteDeps) { } const caseClient = context.case.getCaseClient(); - const theCase = request.body; + const cases = request.body as CasesPatchRequest; try { return response.ok({ - body: await caseClient.update({ request, theCase }), + body: await caseClient.update({ request, cases }), }); } catch (error) { return response.customError(wrapError(error)); 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 6c94f116965d3..8d08c92add39d 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 @@ -8,6 +8,7 @@ import { wrapError, escapeHatch } from '../utils'; import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; +import { CasePostRequest } from '../../../../common/api'; export function initPostCaseApi({ router }: RouteDeps) { router.post( @@ -22,7 +23,7 @@ export function initPostCaseApi({ router }: RouteDeps) { return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); } const caseClient = context.case.getCaseClient(); - const theCase = request.body; + const theCase = request.body as CasePostRequest; try { return response.ok({ From 381e8fcd18d395a2e66ab5d53fb6ac41732780f5 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 22 Oct 2020 10:47:00 +0300 Subject: [PATCH 09/14] Restructure client --- .../server/client/{ => cases}/create.test.ts | 10 +++++----- .../case/server/client/{ => cases}/create.ts | 10 +++++----- .../server/client/{ => cases}/update.test.ts | 20 +++++++++++++------ .../case/server/client/{ => cases}/update.ts | 13 +++++++----- .../client/{ => comments}/add_comment.ts | 10 +++++----- .../plugins/case/server/client/index.test.ts | 14 ++++++------- x-pack/plugins/case/server/client/index.ts | 6 +++--- .../client/{__mocks__/index.ts => mocks.ts} | 0 .../services/{__mocks__/index.ts => mocks.ts} | 12 ++++++++--- 9 files changed, 56 insertions(+), 39 deletions(-) rename x-pack/plugins/case/server/client/{ => cases}/create.test.ts (95%) rename x-pack/plugins/case/server/client/{ => cases}/create.ts (87%) rename x-pack/plugins/case/server/client/{ => cases}/update.test.ts (94%) rename x-pack/plugins/case/server/client/{ => cases}/update.ts (93%) rename x-pack/plugins/case/server/client/{ => comments}/add_comment.ts (93%) rename x-pack/plugins/case/server/client/{__mocks__/index.ts => mocks.ts} (100%) rename x-pack/plugins/case/server/services/{__mocks__/index.ts => mocks.ts} (60%) diff --git a/x-pack/plugins/case/server/client/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts similarity index 95% rename from x-pack/plugins/case/server/client/create.test.ts rename to x-pack/plugins/case/server/client/cases/create.test.ts index a4d9fec0905c4..05381b833762c 100644 --- a/x-pack/plugins/case/server/client/create.test.ts +++ b/x-pack/plugins/case/server/client/cases/create.test.ts @@ -5,17 +5,17 @@ */ import { KibanaRequest } from 'kibana/server'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; -import { ConnectorTypes } from '../../common/api'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { ConnectorTypes } from '../../../common/api'; import { createCaseServiceMock, createConfigureServiceMock, createUserActionServiceMock, -} from '../services/__mocks__'; +} from '../../services/mocks'; import { create } from './create'; -import { CaseClient } from './types'; -import { elasticUser, casePostResponse, postCase, caseConfigureResponse } from './__mocks__'; +import { CaseClient } from '../types'; +import { elasticUser, casePostResponse, postCase, caseConfigureResponse } from '../mocks'; const caseService = createCaseServiceMock(); const caseConfigureService = createConfigureServiceMock(); diff --git a/x-pack/plugins/case/server/client/create.ts b/x-pack/plugins/case/server/client/cases/create.ts similarity index 87% rename from x-pack/plugins/case/server/client/create.ts rename to x-pack/plugins/case/server/client/cases/create.ts index 0d237d3319fe9..21b32a3f8d8e0 100644 --- a/x-pack/plugins/case/server/client/create.ts +++ b/x-pack/plugins/case/server/client/cases/create.ts @@ -9,7 +9,7 @@ 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 { flattenCaseSavedObject, transformNewCase } from '../../routes/api/utils'; import { CasePostRequestRt, @@ -17,14 +17,14 @@ import { excess, CaseResponseRt, CaseResponse, -} from '../../common/api'; -import { buildCaseUserActionItem } from '../services/user_actions/helpers'; +} from '../../../common/api'; +import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { getConnectorFromConfiguration, transformCaseConnectorToEsConnector, -} from '../routes/api/cases/helpers'; +} from '../../routes/api/cases/helpers'; -import { CaseClientCreate, CaseClientFactoryArguments } from './types'; +import { CaseClientCreate, CaseClientFactoryArguments } from '../types'; export const create = ({ savedObjectsClient, diff --git a/x-pack/plugins/case/server/client/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts similarity index 94% rename from x-pack/plugins/case/server/client/update.test.ts rename to x-pack/plugins/case/server/client/cases/update.test.ts index eb73a662b72a2..45b1e5950145a 100644 --- a/x-pack/plugins/case/server/client/update.test.ts +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -5,23 +5,23 @@ */ import { KibanaRequest } from 'kibana/server'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { createCaseServiceMock, createConfigureServiceMock, createUserActionServiceMock, -} from '../services/__mocks__'; -import { mockCases } from '../routes/api/__fixtures__'; +} from '../../services/mocks'; +import { mockCases } from '../../routes/api/__fixtures__'; import { update } from './update'; -import { CaseClient } from './types'; +import { CaseClient } from '../types'; import { elasticUser, patchConnector, patchCases, caseConfigureResponse, getCasesResponse, -} from './__mocks__'; +} from '../mocks'; const caseService = createCaseServiceMock(); const caseConfigureService = createConfigureServiceMock(); @@ -255,7 +255,15 @@ describe('update()', () => { }); test('it throws when case does not exist', async () => { - caseService.getCases.mockResolvedValue({ saved_objects: [{ id: 'not-exists', error: {} }] }); + caseService.getCases.mockResolvedValue({ + saved_objects: [ + { + ...mockCases[0], + id: 'not-exists', + error: { error: 'not found', message: 'not found', statusCode: 404 }, + }, + ], + }); expect.assertions(1); updateHandler({ request, diff --git a/x-pack/plugins/case/server/client/update.ts b/x-pack/plugins/case/server/client/cases/update.ts similarity index 93% rename from x-pack/plugins/case/server/client/update.ts rename to x-pack/plugins/case/server/client/cases/update.ts index 5b943055a8735..ad86bba642b42 100644 --- a/x-pack/plugins/case/server/client/update.ts +++ b/x-pack/plugins/case/server/client/cases/update.ts @@ -9,7 +9,7 @@ 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 { flattenCaseSavedObject } from '../../routes/api/utils'; import { throwErrors, @@ -19,11 +19,14 @@ import { ESCasePatchRequest, CasePatchRequest, CasesResponse, -} from '../../common/api'; -import { buildCaseUserActions } from '../services/user_actions/helpers'; -import { getCaseToUpdate, transformCaseConnectorToEsConnector } from '../routes/api/cases/helpers'; +} from '../../../common/api'; +import { buildCaseUserActions } from '../../services/user_actions/helpers'; +import { + getCaseToUpdate, + transformCaseConnectorToEsConnector, +} from '../../routes/api/cases/helpers'; -import { CaseClientUpdate, CaseClientFactoryArguments } from './types'; +import { CaseClientUpdate, CaseClientFactoryArguments } from '../types'; export const update = ({ savedObjectsClient, diff --git a/x-pack/plugins/case/server/client/add_comment.ts b/x-pack/plugins/case/server/client/comments/add_comment.ts similarity index 93% rename from x-pack/plugins/case/server/client/add_comment.ts rename to x-pack/plugins/case/server/client/comments/add_comment.ts index 2d81f294f2f71..8019dfb153e57 100644 --- a/x-pack/plugins/case/server/client/add_comment.ts +++ b/x-pack/plugins/case/server/client/comments/add_comment.ts @@ -9,7 +9,7 @@ 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 { flattenCaseSavedObject, transformNewComment } from '../../routes/api/utils'; import { throwErrors, @@ -17,11 +17,11 @@ import { CaseResponseRt, CommentRequestRt, CaseResponse, -} from '../../common/api'; -import { buildCommentUserActionItem } from '../services/user_actions/helpers'; +} from '../../../common/api'; +import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; -import { CaseClientAddComment, CaseClientFactoryArguments } from './types'; -import { CASE_SAVED_OBJECT } from '../saved_object_types'; +import { CaseClientAddComment, CaseClientFactoryArguments } from '../types'; +import { CASE_SAVED_OBJECT } from '../../saved_object_types'; export const addComment = ({ savedObjectsClient, diff --git a/x-pack/plugins/case/server/client/index.test.ts b/x-pack/plugins/case/server/client/index.test.ts index 941f5ca7d9409..ed75da8b1d522 100644 --- a/x-pack/plugins/case/server/client/index.test.ts +++ b/x-pack/plugins/case/server/client/index.test.ts @@ -10,15 +10,15 @@ import { createCaseServiceMock, createConfigureServiceMock, createUserActionServiceMock, -} from '../services/__mocks__'; +} from '../services/mocks'; -import { create } from './create'; -import { update } from './update'; -import { addComment } from './add_comment'; +import { create } from './cases/create'; +import { update } from './cases/update'; +import { addComment } from './comments/add_comment'; -jest.mock('./create'); -jest.mock('./update'); -jest.mock('./add_comment'); +jest.mock('./cases/create'); +jest.mock('./cases/update'); +jest.mock('./comments/add_comment'); const caseService = createCaseServiceMock(); const caseConfigureService = createConfigureServiceMock(); diff --git a/x-pack/plugins/case/server/client/index.ts b/x-pack/plugins/case/server/client/index.ts index 8c746c7d053d9..ad238729cbd92 100644 --- a/x-pack/plugins/case/server/client/index.ts +++ b/x-pack/plugins/case/server/client/index.ts @@ -5,9 +5,9 @@ */ import { CaseClientFactoryArguments, CaseClient } from './types'; -import { create } from './create'; -import { update } from './update'; -import { addComment } from './add_comment'; +import { create } from './cases/create'; +import { update } from './cases/update'; +import { addComment } from './comments/add_comment'; export { CaseClient } from './types'; diff --git a/x-pack/plugins/case/server/client/__mocks__/index.ts b/x-pack/plugins/case/server/client/mocks.ts similarity index 100% rename from x-pack/plugins/case/server/client/__mocks__/index.ts rename to x-pack/plugins/case/server/client/mocks.ts diff --git a/x-pack/plugins/case/server/services/__mocks__/index.ts b/x-pack/plugins/case/server/services/mocks.ts similarity index 60% rename from x-pack/plugins/case/server/services/__mocks__/index.ts rename to x-pack/plugins/case/server/services/mocks.ts index f8420cbe4e37d..287f80a60ab07 100644 --- a/x-pack/plugins/case/server/services/__mocks__/index.ts +++ b/x-pack/plugins/case/server/services/mocks.ts @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -export const createCaseServiceMock = () => ({ +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(), @@ -23,7 +29,7 @@ export const createCaseServiceMock = () => ({ patchComments: jest.fn(), }); -export const createConfigureServiceMock = () => ({ +export const createConfigureServiceMock = (): CaseConfigureServiceMock => ({ delete: jest.fn(), get: jest.fn(), find: jest.fn(), @@ -31,7 +37,7 @@ export const createConfigureServiceMock = () => ({ post: jest.fn(), }); -export const createUserActionServiceMock = () => ({ +export const createUserActionServiceMock = (): CaseUserActionServiceMock => ({ getUserActions: jest.fn(), postUserActions: jest.fn(), }); From b99c2d72341899f6bed3da303f78bbaec1e877f8 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 22 Oct 2020 12:33:26 +0300 Subject: [PATCH 10/14] Add tests --- .../case/server/client/__fixtures__/index.ts | 81 +++ .../case/server/client/cases/create.test.ts | 352 +++++++++--- .../case/server/client/cases/update.test.ts | 520 ++++++++++-------- .../client/comments/add_comment.test.ts | 223 ++++++++ x-pack/plugins/case/server/client/mocks.ts | 134 ++--- x-pack/plugins/case/server/plugin.ts | 7 +- .../__fixtures__/create_mock_so_repository.ts | 10 +- .../api/__fixtures__/mock_saved_objects.ts | 11 +- .../routes/api/__fixtures__/route_contexts.ts | 3 + .../api/cases/comments/post_comment.test.ts | 167 +++--- .../routes/api/cases/patch_cases.test.ts | 278 ++-------- .../server/routes/api/cases/post_case.test.ts | 179 ++---- .../plugins/case/server/routes/api/utils.ts | 2 +- 13 files changed, 1082 insertions(+), 885 deletions(-) create mode 100644 x-pack/plugins/case/server/client/__fixtures__/index.ts create mode 100644 x-pack/plugins/case/server/client/comments/add_comment.test.ts diff --git a/x-pack/plugins/case/server/client/__fixtures__/index.ts b/x-pack/plugins/case/server/client/__fixtures__/index.ts new file mode 100644 index 0000000000000..85f666518e787 --- /dev/null +++ b/x-pack/plugins/case/server/client/__fixtures__/index.ts @@ -0,0 +1,81 @@ +/* + * 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 { SavedObject } from 'kibana/server'; +import { CasePostRequest, ConnectorTypes, ESCaseAttributes } from '../../../common/api'; +import { mockCases, mockCaseConfigureFind } from '../../routes/api/__fixtures__'; + +const createdAt = mockCases[0].attributes.created_at; +const updatedAt = mockCases[0].attributes.updated_at; + +export const elasticUser = mockCases[0].attributes.created_by; + +export const comment = { + comment: 'Solve this fast!', + id: 'comment-1', + createdAt, + createdBy: elasticUser, + pushedAt: null, + pushedBy: null, + updatedAt: null, + updatedBy: null, + version: 'WzQ3LDFc', +}; + +export const tags: string[] = ['defacement']; +export const connector = mockCases[0].attributes.connector; + +export 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, + }, +}; + +export const patchCases = { + cases: [ + { + id: mockCases[0].id, + title: 'Title updated', + description: 'Description updated', + version: mockCases[0].version ?? 'WzAsMV0=', + }, + ], +}; + +export const patchConnector = { + cases: [ + { + id: mockCases[0].id, + connector: { id: 'jira', name: 'jira', type: ConnectorTypes.jira, fields: null }, + version: mockCases[0].version ?? 'WzAsMV0=', + }, + ], +}; + +export const casePostResponse = (attributes: CasePostRequest): SavedObject => ({ + ...mockCases[0], + attributes: { + ...mockCases[0].attributes, + ...attributes, + updated_at: null, + updated_by: null, + }, +}); + +export const caseConfigureResponse = mockCaseConfigureFind[0]; + +export const getCasesResponse = { + saved_objects: [mockCases[0]], + total: 1, + per_page: 20, + page: 1, +}; diff --git a/x-pack/plugins/case/server/client/cases/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts index 05381b833762c..692e7e13abefc 100644 --- a/x-pack/plugins/case/server/client/cases/create.test.ts +++ b/x-pack/plugins/case/server/client/cases/create.test.ts @@ -5,53 +5,61 @@ */ import { KibanaRequest } from 'kibana/server'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { ConnectorTypes } from '../../../common/api'; -import { - createCaseServiceMock, - createConfigureServiceMock, - createUserActionServiceMock, -} from '../../services/mocks'; - -import { create } from './create'; -import { CaseClient } from '../types'; -import { elasticUser, casePostResponse, postCase, caseConfigureResponse } from '../mocks'; - -const caseService = createCaseServiceMock(); -const caseConfigureService = createConfigureServiceMock(); -const userActionService = createUserActionServiceMock(); -const savedObjectsClient = savedObjectsClientMock.create(); -const request = {} as KibanaRequest; -describe('create()', () => { - let createHandler: CaseClient['create']; +import { + createMockSavedObjectsRepository, + mockCaseConfigure, + mockCases, +} from '../../routes/api/__fixtures__'; +import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; - beforeEach(() => { - jest.resetAllMocks(); - caseService.getUser.mockResolvedValue(elasticUser); - caseConfigureService.find.mockResolvedValue(caseConfigureResponse); - caseService.postNewCase.mockResolvedValue(casePostResponse); +const request = {} as KibanaRequest; - createHandler = create({ - savedObjectsClient, - caseService, - caseConfigureService, - userActionService, - }); +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 res = await createHandler({ request, theCase: postCase }); + 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 }, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseConfigureSavedObject: mockCaseConfigure, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.create({ request, theCase: postCase }); + expect(res).toEqual({ - id: 'mock-id-1', + id: 'mock-it', comments: [], totalComment: 0, closed_at: null, closed_by: null, - connector: { id: 'none', name: 'none', type: '.none', fields: 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: 'elastic', email: 'testemail@elastic.co', username: 'elastic' }, + 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', @@ -59,23 +67,108 @@ describe('create()', () => { tags: ['defacement'], updated_at: null, updated_by: null, - version: 'WzAsMV0=', + 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 () => { - caseConfigureService.find.mockResolvedValue({ ...caseConfigureResponse, saved_objects: [] }); + 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({ request, 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({ request, theCase: postCase }); - const res = await createHandler({ request, theCase: postCase }); expect(res).toEqual({ - id: 'mock-id-1', + id: 'mock-it', comments: [], totalComment: 0, closed_at: null, closed_by: null, - connector: { id: 'none', name: 'none', type: '.none', fields: null }, + 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' }, + 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', @@ -83,7 +176,7 @@ describe('create()', () => { tags: ['defacement'], updated_at: null, updated_by: null, - version: 'WzAsMV0=', + version: 'WzksMV0=', }); }); }); @@ -91,74 +184,159 @@ describe('create()', () => { describe('unhappy path', () => { test('it throws when missing title', async () => { expect.assertions(1); - caseConfigureService.find.mockResolvedValue({ ...caseConfigureResponse, saved_objects: [] }); - createHandler({ - request, - // @ts-expect-error - theCase: { - description: 'desc', - tags: [], - connector: postCase.connector, + 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, }, - }).catch((e) => expect(e).not.toBeNull()); + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ request, theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); }); test('it throws when missing description', async () => { expect.assertions(1); - caseConfigureService.find.mockResolvedValue({ ...caseConfigureResponse, saved_objects: [] }); - createHandler({ - request, - // @ts-expect-error - theCase: { - title: 'title', - tags: [], - connector: postCase.connector, + const postCase = { + title: 'a title', + tags: ['defacement'], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, }, - }).catch((e) => expect(e).not.toBeNull()); + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ request, theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); }); test('it throws when missing tags', async () => { expect.assertions(1); - caseConfigureService.find.mockResolvedValue({ ...caseConfigureResponse, saved_objects: [] }); - createHandler({ - request, - // @ts-expect-error - theCase: { - title: 'title', - description: 'desc', - connector: postCase.connector, + 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, }, - }).catch((e) => expect(e).not.toBeNull()); + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ request, theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); }); test('it throws when missing connector ', async () => { expect.assertions(1); - caseConfigureService.find.mockResolvedValue({ ...caseConfigureResponse, saved_objects: [] }); - createHandler({ - request, + 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 - theCase: { title: 'a title', description: 'desc', tags: [] }, - }).catch((e) => expect(e).not.toBeNull()); + .create({ request, theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); }); test('it throws when connector missing the right fields', async () => { expect.assertions(1); - caseConfigureService.find.mockResolvedValue({ ...caseConfigureResponse, saved_objects: [] }); - createHandler({ - request, - theCase: { - title: 'a title', - description: 'desc', - tags: [], - connector: { - id: 'jira', - name: 'jira', - type: ConnectorTypes.jira, - // @ts-expect-error - fields: {}, - }, + 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: {}, }, - }).catch((e) => expect(e).not.toBeNull()); + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ request, 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({ request, 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({ request, theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); }); }); }); diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts index 45b1e5950145a..55e6ab1bf79fa 100644 --- a/x-pack/plugins/case/server/client/cases/update.test.ts +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -5,302 +5,382 @@ */ import { KibanaRequest } from 'kibana/server'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { ConnectorTypes } from '../../../common/api'; import { - createCaseServiceMock, - createConfigureServiceMock, - createUserActionServiceMock, -} from '../../services/mocks'; -import { mockCases } from '../../routes/api/__fixtures__'; - -import { update } from './update'; -import { CaseClient } from '../types'; -import { - elasticUser, - patchConnector, - patchCases, - caseConfigureResponse, - getCasesResponse, -} from '../mocks'; - -const caseService = createCaseServiceMock(); -const caseConfigureService = createConfigureServiceMock(); -const userActionService = createUserActionServiceMock(); -const savedObjectsClient = savedObjectsClientMock.create(); -const request = {} as KibanaRequest; + createMockSavedObjectsRepository, + mockCaseNoConnectorId, + mockCases, +} from '../../routes/api/__fixtures__'; +import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; -describe('update()', () => { - let updateHandler: CaseClient['update']; - beforeEach(() => { - jest.resetAllMocks(); - const { id, version, ...patchCaseAttributes } = patchCases.cases[0]; - caseService.getUser.mockResolvedValue(elasticUser); - caseService.getCases.mockResolvedValue(getCasesResponse); - caseService.patchCases.mockResolvedValue({ - saved_objects: [ - { - ...mockCases[0], - version: 'WzAsMV1=', - attributes: { ...mockCases[0].attributes, ...patchCaseAttributes }, - }, - ], - }); - caseConfigureService.find.mockResolvedValue(caseConfigureResponse); +const request = {} as KibanaRequest; - updateHandler = update({ - savedObjectsClient, - caseService, - caseConfigureService, - userActionService, - }); +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 updates the case correctly', async () => { - const res = await updateHandler({ request, cases: patchCases }); + 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({ request, cases: patchCases }); + expect(res).toEqual([ { - id: 'mock-id-1', + closed_at: '2019-11-25T21:54:48.952Z', + closed_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, comments: [], - totalComment: 0, - closed_at: null, - closed_by: null, - connector: { id: 'none', name: 'none', type: '.none', fields: null }, + 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: 'Description updated', + 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, - title: 'Title updated', - status: 'open', + status: 'closed', tags: ['defacement'], + title: 'Super Bad Security Issue', + totalComment: 0, updated_at: '2019-11-25T21:54:48.952Z', - updated_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', + 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', }, - version: 'WzAsMV1=', + references: [ + { + id: 'mock-id-1', + name: 'associated-cases', + type: 'cases', + }, + ], }, ]); }); - test('it updates the connector correctly', async () => { - const { - id: patchConnectorId, - version: patchConnectorVersion, - ...patchConnectorAttributes - } = patchConnector.cases[0]; - - caseService.patchCases.mockResolvedValue({ - saved_objects: [ + test('it opens the case correctly', async () => { + const patchCases = { + cases: [ { - ...mockCases[0], - version: 'WzAsMV1=', - attributes: { ...mockCases[0].attributes, ...patchConnectorAttributes }, + 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 res = await updateHandler({ request, cases: patchConnector }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.update({ request, cases: patchCases }); + expect(res).toEqual([ { - id: 'mock-id-1', - comments: [], - totalComment: 0, closed_at: null, closed_by: null, - connector: { id: 'jira', name: 'jira', type: '.jira', fields: null }, + comments: [], + 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' }, + 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, - title: 'Super Bad Security Issue', status: 'open', tags: ['defacement'], + title: 'Super Bad Security Issue', + totalComment: 0, updated_at: '2019-11-25T21:54:48.952Z', - updated_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - version: 'WzAsMV1=', + updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + version: 'WzE3LDFd', }, ]); }); - test('it updates the status correctly: closed', async () => { - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), - })); + test('it updates a case without a connector.id', async () => { + const patchCases = { + cases: [ + { + id: 'mock-no-connector_id', + status: 'closed' as const, + version: 'WzAsMV0=', + }, + ], + }; - await updateHandler({ - request, - cases: { - cases: [ - { - id: 'mock-id-1', - version: 'WzAsMV0=', - status: 'closed', - }, - ], - }, + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: [mockCaseNoConnectorId], }); - expect(caseService.patchCases.mock.calls[0][0].cases[0].updatedAttributes).toEqual({ - status: 'closed', - closed_at: '2019-11-25T21:54:48.952Z', - closed_by: { - email: 'testemail@elastic.co', - full_name: 'elastic', - username: 'elastic', - }, - updated_at: '2019-11-25T21:54:48.952Z', - updated_by: { - email: 'testemail@elastic.co', - full_name: 'elastic', - username: 'elastic', + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.update({ request, 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 status correctly: open', async () => { - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), - })); - - caseService.getCases.mockResolvedValue({ - ...getCasesResponse, - saved_objects: [ - { ...mockCases[0], attributes: { ...mockCases[0].attributes, status: 'closed' } }, + 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=', + }, ], - }); + }; - await updateHandler({ - request, - cases: { - cases: [ - { - id: 'mock-id-1', - version: 'WzAsMV0=', - status: 'open', - }, - ], - }, + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, }); - expect(caseService.patchCases.mock.calls[0][0].cases[0].updatedAttributes).toEqual({ - status: 'open', - closed_at: null, - closed_by: null, - updated_at: '2019-11-25T21:54:48.952Z', - updated_by: { - email: 'testemail@elastic.co', - full_name: 'elastic', - username: 'elastic', + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.update({ request, 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); - updateHandler({ - request, - cases: { - cases: [ - // @ts-expect-error - { - title: 'update', + const patchCases = { + cases: [ + { + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, }, - ], - }, - }).catch((e) => expect(e).not.toBeNull()); + version: 'WzUsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .update({ request, cases: patchCases }) + .catch((e) => expect(e).not.toBeNull()); }); test('it throws when missing version', async () => { expect.assertions(1); - updateHandler({ - request, - cases: { - cases: [ - // @ts-expect-error - { - id: '123', - title: 'update', + const patchCases = { + cases: [ + { + id: 'mock-id-3', + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, }, - ], - }, - }).catch((e) => expect(e).not.toBeNull()); + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .update({ request, cases: patchCases }) + .catch((e) => expect(e).not.toBeNull()); }); test('it throws when fields are identical', async () => { expect.assertions(1); - updateHandler({ - request, - cases: { - cases: [ - { - id: 'mock-id-1', - version: 'WzAsMV0=', - title: 'Super Bad Security Issue', - }, - ], - }, - }).catch((e) => - expect(e.message).toBe('All update fields are identical to current version.') - ); + 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({ request, 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 () => { - caseService.getCases.mockResolvedValue({ - saved_objects: [ + const patchCases = { + cases: [ { - ...mockCases[0], id: 'not-exists', - error: { error: 'not found', message: 'not found', statusCode: 404 }, + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + version: 'WzUsMV0=', }, ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, }); - expect.assertions(1); - updateHandler({ - request, - cases: { - cases: [ - { - id: 'not-exists', - version: 'WzAsMV0=', - title: 'Super Bad Security Issue', - }, - ], - }, - }).catch((e) => - expect(e.message).toBe( - 'These cases not-exists do not exist. Please check you have the correct ids.' - ) - ); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .update({ request, 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); - updateHandler({ - request, - cases: { - cases: [ - { - id: 'mock-id-1', - version: 'WzAsMV1=', - title: 'Super Bad Security Issue', - }, - ], - }, - }).catch((e) => - expect(e.message).toBe( - 'These cases mock-id-1 has been updated. Please refresh before saving additional updates.' - ) - ); + 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({ request, 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/comments/add_comment.test.ts b/x-pack/plugins/case/server/client/comments/add_comment.test.ts new file mode 100644 index 0000000000000..08cb2ae4f6481 --- /dev/null +++ b/x-pack/plugins/case/server/client/comments/add_comment.test.ts @@ -0,0 +1,223 @@ +/* + * 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 { + createMockSavedObjectsRepository, + mockCaseComments, + mockCases, +} from '../../routes/api/__fixtures__'; +import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; + +const request = {} as KibanaRequest; + +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({ + request, + 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({ + request, + 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); + const res = await caseClient.client.addComment({ + request, + 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({ + request, + 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({ + request, + 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({ + request, + 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({ + request, + 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/mocks.ts b/x-pack/plugins/case/server/client/mocks.ts index 6bcadd533dba8..ad2815269bea9 100644 --- a/x-pack/plugins/case/server/client/mocks.ts +++ b/x-pack/plugins/case/server/client/mocks.ts @@ -4,95 +4,47 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConnectorTypes } from '../../../common/api'; -import { mockCases, mockCaseConfigure } from '../../routes/api/__fixtures__'; - -const createdAt = mockCases[0].attributes.created_at; -const updatedAt = mockCases[0].attributes.updated_at; - -export const elasticUser = mockCases[0].attributes.created_by; - -export const comment = { - comment: 'Solve this fast!', - id: 'comment-1', - createdAt, - createdBy: elasticUser, - pushedAt: null, - pushedBy: null, - updatedAt: null, - updatedBy: null, - version: 'WzQ3LDFc', -}; - -export const tags: string[] = ['defacement']; -export const connector = mockCases[0].attributes.connector; - -export const postCase = { - title: mockCases[0].attributes.title, - description: mockCases[0].attributes.description, - tags, - connector: { ...connector, fields: null }, -}; - -export const patchCases = { - cases: [ - { - id: mockCases[0].id, - title: 'Title updated', - description: 'Description updated', - version: mockCases[0].version ?? 'WzAsMV0=', - }, - ], -}; - -export const patchConnector = { - cases: [ - { - id: mockCases[0].id, - connector: { id: 'jira', name: 'jira', type: ConnectorTypes.jira, fields: null }, - version: mockCases[0].version ?? 'WzAsMV0=', - }, - ], -}; - -export const theCase = { - closedAt: null, - closedBy: null, - id: 'case-1', - comments: [comment], - createdAt, - createdBy: elasticUser, - connector, - description: 'This is a brand new case of a bad meanie defacing data', - externalService: null, - status: 'open', - tags, - title: 'Super Bad Security Issue', - totalComment: 1, - updatedAt, - updatedBy: elasticUser, - version: 'WzQ3LDFd', -}; - -export const casePostResponse = { - ...mockCases[0], - attributes: { - ...mockCases[0].attributes, - updated_at: null, - updated_by: null, - }, -}; - -export const caseConfigureResponse = { - saved_objects: mockCaseConfigure[0], - total: 1, - per_page: 20, - page: 1, -}; - -export const getCasesResponse = { - saved_objects: [mockCases[0]], - total: 1, - per_page: 20, - page: 1, +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 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, + caseService, + caseConfigureService, + userActionService, + }), + services: { userActionService }, + }; }; diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index a25207fde85b2..3b2032143ece4 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -12,7 +12,7 @@ import { PluginInitializerContext, RequestHandler, } from 'kibana/server'; -import { CoreSetup } from 'src/core/server'; +import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; import { APP_ID } from '../common/constants'; @@ -96,13 +96,12 @@ export class CasePlugin { }); } - public async start(core: CoreSetup) { + public async start(core: CoreStart) { this.log.debug(`Starting Case Workflow`); - const [{ savedObjects }] = await core.getStartServices(); const getCaseClientWithRequest = async (request: KibanaRequest) => { return createCaseClient({ - savedObjectsClient: savedObjects.getScopedClient(request), + savedObjectsClient: core.savedObjects.getScopedClient(request), caseService: this.caseService!, caseConfigureService: this.caseConfigureService!, userActionService: this.userActionService!, 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..f2b2784c427d1 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 @@ -6,11 +6,13 @@ import { RequestHandlerContext } from 'src/core/server'; import { actionsClientMock } from '../../../../../actions/server/mocks'; +import { createCaseClientMock } from '../../../client/mocks'; import { getActions } from '../__mocks__/request_responses'; export const createRouteContext = (client: any) => { const actionsMock = actionsClientMock.create(); actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); + const caseMock = createCaseClientMock(); return ({ core: { @@ -19,5 +21,6 @@ export const createRouteContext = (client: any) => { }, }, actions: { getActionsClient: () => actionsMock }, + case: { getCaseClient: () => caseMock }, } as unknown) as RequestHandlerContext; }; 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..0c81aaf6ae11f 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 @@ -14,8 +14,10 @@ import { mockCases, mockCaseComments, } from '../../__fixtures__'; -import { initPostCommentApi } from './post_comment'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CaseClient } from '../../../../client'; +import { initPostCommentApi } from './post_comment'; +import { ConnectorTypes } from '../../../../../common/api'; describe('POST comment', () => { let routeHandler: RequestHandler; @@ -23,82 +25,77 @@ describe('POST comment', () => { routeHandler = await createRoute(initPostCommentApi, 'post'); const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + toISOString: jest.fn().mockReturnValue('2020-10-23T21:54:48.952Z'), })); }); - it(`Posts a new comment`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: { - comment: 'Wow, good luck catching that bad meanie!', - }, - }); - - const theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.comments[response.payload.comments.length - 1].id).toEqual( - 'mock-comment' - ); - }); - it(`Returns an error if the case does not exist`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'this-is-not-real', + it(`it adds a new comment`, async () => { + const addCommentResult = { + id: 'mock-id-1', + version: 'WzE3LDFd', + comments: [ + { + id: 'mock-comment-1', + version: 'WzEsMV0=', + comment: 'Wow, good luck catching that bad meanie!', + created_at: '2019-11-25T21:55:00.177Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + pushed_at: null, + pushed_by: null, + updated_at: '2019-11-25T21:55:00.177Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }, + { + id: 'mock-comment', + version: 'WzksMV0=', + 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', + }, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + }, + ], + totalComment: 2, + closed_at: null, + closed_by: null, + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, }, - body: { - comment: 'Wow, good luck catching that bad meanie!', - }, - }); - - const theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(theContext, request, kibanaResponseFactory); - 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, - method: 'post', - params: { - case_id: 'mock-id-1', + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', }, - body: { - comment: 'Throw an error', + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'open' as const, + tags: ['defacement'], + updated_at: '2020-10-23T21:54:48.952Z', + updated_by: { + username: 'awesome', + full_name: 'Awesome D00d', + email: 'd00d@awesome.com', }, - }); - - const theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(theContext, request, kibanaResponseFactory); - 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); + }; const request = httpServerMock.createKibanaRequest({ path: CASE_COMMENTS_URL, @@ -111,29 +108,23 @@ describe('POST comment', () => { }, }); - const theContext = createRouteContext( + const context = createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); + const caseClient = context.case!.getCaseClient() as jest.Mocked; + caseClient.addComment.mockResolvedValueOnce(addCommentResult); + const response = await routeHandler(context, request, kibanaResponseFactory); - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.comments[response.payload.comments.length - 1]).toEqual({ - comment: 'Wow, good luck catching that bad meanie!', - created_at: '2019-11-25T21: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=', + expect(caseClient.addComment).toHaveBeenCalledTimes(1); + expect(caseClient.addComment).toHaveBeenCalledWith({ + request, + caseId: request.params.case_id, + comment: request.body, }); + expect(response.status).toEqual(200); + expect(response.payload).toEqual(addCommentResult); }); }); 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..0d4180f01d8cb 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 @@ -12,11 +12,10 @@ import { createRoute, createRouteContext, mockCases, - mockCaseComments, } from '../__fixtures__'; import { initPatchCasesApi } from './patch_cases'; -import { mockCaseConfigure, mockCaseNoConnectorId } from '../__fixtures__/mock_saved_objects'; import { ConnectorTypes } from '../../../../common/api/connectors'; +import { CaseClient } from '../../../client'; describe('PATCH cases', () => { let routeHandler: RequestHandler; @@ -28,221 +27,31 @@ describe('PATCH cases', () => { })); }); - it(`Close a case`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-1', - status: 'closed', - version: 'WzAsMV0=', - }, - ], - }, - }); - - const theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload).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', - }, - ]); - }); - - it(`Open a case`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-4', - status: 'open', - version: 'WzUsMV0=', - }, - ], - }, - }); - - const theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseConfigureSavedObject: mockCaseConfigure, - }) - ); - - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload).toEqual([ - { - closed_at: null, - closed_by: null, - comments: [], - connector: { - id: '123', - name: 'My connector', - type: '.jira', - fields: { issueType: 'Task', priority: 'High', parent: null }, - }, - created_at: '2019-11-25T22:32:17.947Z', - created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' }, - description: 'Oh no, a bad meanie going LOLBins all over the place!', - id: 'mock-id-4', - external_service: null, - status: 'open', - tags: ['LOLBins'], - title: 'Another bad one', - totalComment: 0, - updated_at: '2019-11-25T21:54:48.952Z', - updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, - version: 'WzE3LDFd', - }, - ]); - }); - - it(`Patches a case without a connector.id`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-no-connector_id', - status: 'closed', - version: 'WzAsMV0=', - }, - ], - }, - }); - - const theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: [mockCaseNoConnectorId], - }) - ); - - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload[0].connector.id).toEqual('none'); - }); - - it(`Patches a case with a connector.id`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-3', - status: 'closed', - version: 'WzUsMV0=', - }, - ], - }, - }); - - const theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload[0].connector.id).toEqual('123'); - }); - - it(`Change connector`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-3', - connector: { - id: '456', - name: 'My connector 2', - type: '.jira', - fields: { issueType: 'Bug', priority: 'Low', parent: null }, - }, - version: 'WzUsMV0=', - }, - ], - }, - }); - - const theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload[0].connector).toEqual({ - id: '456', - name: 'My connector 2', - type: '.jira', - fields: { issueType: 'Bug', priority: 'Low', parent: null }, - }); - }); - - it(`Fails with 409 if version does not match`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-1', - case: { status: 'closed' }, - version: 'badv=', - }, - ], + it(`it updates a new case`, async () => { + const patchResult = { + 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', + }; - const theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(409); - }); - - it(`Fails with 406 if updated field is unchanged`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases', method: 'patch', @@ -250,47 +59,26 @@ describe('PATCH cases', () => { cases: [ { id: 'mock-id-1', - case: { status: 'open' }, + status: 'closed' as const, version: 'WzAsMV0=', }, ], }, }); - const theContext = createRouteContext( + const context = createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(406); - }); - - it(`Returns an error if updateCase throws`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-does-not-exist', - status: 'closed', - version: 'WzAsMV0=', - }, - ], - }, - }); - - const theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); + const caseClient = context.case!.getCaseClient() as jest.Mocked; + caseClient.update.mockResolvedValueOnce(patchResult); + const response = await routeHandler(context, request, kibanaResponseFactory); - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(404); - expect(response.payload.isBoom).toEqual(true); + expect(caseClient.update).toHaveBeenCalledTimes(1); + expect(caseClient.update).toHaveBeenCalledWith({ request, cases: request.body }); + expect(response.status).toEqual(200); + expect(response.payload).toEqual(patchResult); }); }); 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..3d80be79308f9 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 @@ -15,8 +15,8 @@ import { } from '../__fixtures__'; import { initPostCaseApi } from './post_case'; import { CASES_URL } from '../../../../common/constants'; -import { mockCaseConfigure } from '../__fixtures__/mock_saved_objects'; import { ConnectorTypes } from '../../../../common/api/connectors'; +import { CaseClient } from '../../../client'; describe('POST cases', () => { let routeHandler: RequestHandler; @@ -28,123 +28,30 @@ describe('POST cases', () => { })); }); - it(`Posts a new case, no connector configured`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASES_URL, - method: 'post', - body: { - 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 theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.id).toEqual('mock-it'); - expect(response.payload.created_by.username).toEqual('awesome'); - expect(response.payload.connector).toEqual({ - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }); - }); - - it(`Posts a new case, connector provided`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASES_URL, - method: 'post', - body: { - 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 }, - }, - }, - }); - - const theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseConfigureSavedObject: mockCaseConfigure, - }) - ); - - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.connector).toEqual({ - id: '123', - name: 'Jira', - type: '.jira', - fields: { issueType: 'Task', priority: 'High', parent: null }, - }); - }); - - it(`Error if you passing status for a new case`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASES_URL, - method: 'post', - body: { - description: 'This is a brand new case of a bad meanie defacing data', - title: 'Super Bad Security Issue', - status: 'open', - tags: ['defacement'], - connector: null, - }, - }); - - const theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - }); - - it(`Returns an error if postNewCase throws`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASES_URL, - method: 'post', - body: { - description: 'Throw an error', - title: 'Super Bad Security Issue', - tags: ['error'], - connector: null, + it(`it creates a new case`, async () => { + const createResult = { + id: 'mock-it', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, }, - }); - - const theContext = createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); - }); - - it(`Allow user to create case without authentication`, async () => { - routeHandler = await createRoute(initPostCaseApi, 'post', true); + 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' as const, + tags: ['defacement'], + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }; const request = httpServerMock.createKibanaRequest({ path: CASES_URL, @@ -162,41 +69,19 @@ describe('POST cases', () => { }, }); - const theContext = createRouteContext( + const context = createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, - caseConfigureSavedObject: mockCaseConfigure, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const caseClient = context.case!.getCaseClient() as jest.Mocked; + caseClient.create.mockResolvedValueOnce(createResult); + const response = await routeHandler(context, request, kibanaResponseFactory); + + expect(caseClient.create).toHaveBeenCalledTimes(1); + expect(caseClient.create).toHaveBeenCalledWith({ request, theCase: request.body }); expect(response.status).toEqual(200); - expect(response.payload).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: null, - full_name: null, - username: null, - }, - description: 'This is a brand new case of a bad meanie defacing data', - external_service: null, - id: 'mock-it', - status: 'open', - tags: ['defacement'], - title: 'Super Bad Security Issue', - totalComment: 0, - updated_at: null, - updated_by: null, - version: 'WzksMV0=', - }); + expect(response.payload).toEqual(createResult); }); }); 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>; From 42ba4b6b3b9edba07c75477af412a8b27ddb5e15 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 23 Oct 2020 13:48:58 +0300 Subject: [PATCH 11/14] Fix types --- .../case/server/client/__fixtures__/index.ts | 81 ------------------- .../case/server/client/cases/create.test.ts | 4 +- .../case/server/client/cases/update.test.ts | 6 +- .../client/comments/add_comment.test.ts | 8 +- x-pack/plugins/case/server/client/types.ts | 6 -- .../routes/api/cases/patch_cases.test.ts | 50 ++++++------ 6 files changed, 35 insertions(+), 120 deletions(-) delete mode 100644 x-pack/plugins/case/server/client/__fixtures__/index.ts diff --git a/x-pack/plugins/case/server/client/__fixtures__/index.ts b/x-pack/plugins/case/server/client/__fixtures__/index.ts deleted file mode 100644 index 85f666518e787..0000000000000 --- a/x-pack/plugins/case/server/client/__fixtures__/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SavedObject } from 'kibana/server'; -import { CasePostRequest, ConnectorTypes, ESCaseAttributes } from '../../../common/api'; -import { mockCases, mockCaseConfigureFind } from '../../routes/api/__fixtures__'; - -const createdAt = mockCases[0].attributes.created_at; -const updatedAt = mockCases[0].attributes.updated_at; - -export const elasticUser = mockCases[0].attributes.created_by; - -export const comment = { - comment: 'Solve this fast!', - id: 'comment-1', - createdAt, - createdBy: elasticUser, - pushedAt: null, - pushedBy: null, - updatedAt: null, - updatedBy: null, - version: 'WzQ3LDFc', -}; - -export const tags: string[] = ['defacement']; -export const connector = mockCases[0].attributes.connector; - -export 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, - }, -}; - -export const patchCases = { - cases: [ - { - id: mockCases[0].id, - title: 'Title updated', - description: 'Description updated', - version: mockCases[0].version ?? 'WzAsMV0=', - }, - ], -}; - -export const patchConnector = { - cases: [ - { - id: mockCases[0].id, - connector: { id: 'jira', name: 'jira', type: ConnectorTypes.jira, fields: null }, - version: mockCases[0].version ?? 'WzAsMV0=', - }, - ], -}; - -export const casePostResponse = (attributes: CasePostRequest): SavedObject => ({ - ...mockCases[0], - attributes: { - ...mockCases[0].attributes, - ...attributes, - updated_at: null, - updated_by: null, - }, -}); - -export const caseConfigureResponse = mockCaseConfigureFind[0]; - -export const getCasesResponse = { - saved_objects: [mockCases[0]], - total: 1, - per_page: 20, - page: 1, -}; diff --git a/x-pack/plugins/case/server/client/cases/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts index 692e7e13abefc..5e465ed7fdc9a 100644 --- a/x-pack/plugins/case/server/client/cases/create.test.ts +++ b/x-pack/plugins/case/server/client/cases/create.test.ts @@ -5,7 +5,7 @@ */ import { KibanaRequest } from 'kibana/server'; -import { ConnectorTypes } from '../../../common/api'; +import { ConnectorTypes, CasePostRequest } from '../../../common/api'; import { createMockSavedObjectsRepository, @@ -37,7 +37,7 @@ describe('create', () => { type: ConnectorTypes.jira, fields: { issueType: 'Task', priority: 'High', parent: null }, }, - }; + } as CasePostRequest; const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts index 55e6ab1bf79fa..9b9f69d5ed093 100644 --- a/x-pack/plugins/case/server/client/cases/update.test.ts +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -5,7 +5,7 @@ */ import { KibanaRequest } from 'kibana/server'; -import { ConnectorTypes } from '../../../common/api'; +import { ConnectorTypes, CasesPatchRequest } from '../../../common/api'; import { createMockSavedObjectsRepository, mockCaseNoConnectorId, @@ -190,7 +190,7 @@ describe('update', () => { }); test('it updates the connector correctly', async () => { - const patchCases = { + const patchCases = ({ cases: [ { id: 'mock-id-3', @@ -203,7 +203,7 @@ describe('update', () => { version: 'WzUsMV0=', }, ], - }; + } as unknown) as CasesPatchRequest; const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, diff --git a/x-pack/plugins/case/server/client/comments/add_comment.test.ts b/x-pack/plugins/case/server/client/comments/add_comment.test.ts index 08cb2ae4f6481..77e2d6d8afec8 100644 --- a/x-pack/plugins/case/server/client/comments/add_comment.test.ts +++ b/x-pack/plugins/case/server/client/comments/add_comment.test.ts @@ -39,8 +39,8 @@ describe('addComment', () => { }); expect(res.id).toEqual('mock-id-1'); - expect(res.totalComment).toEqual(res.comments.length); - expect(res.comments[res.comments.length - 1]).toEqual({ + 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: { @@ -85,7 +85,7 @@ describe('addComment', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - const res = await caseClient.client.addComment({ + await caseClient.client.addComment({ request, caseId: 'mock-id-1', comment: { comment: 'Wow, good luck catching that bad meanie!' }, @@ -137,7 +137,7 @@ describe('addComment', () => { }); expect(res.id).toEqual('mock-id-1'); - expect(res.comments[res.comments.length - 1]).toEqual({ + 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: { diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index 5c6e5066e6d5d..ef1dd94dd50c2 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -47,9 +47,3 @@ export interface CaseClient { update: (args: CaseClientUpdate) => Promise; addComment: (args: CaseClientAddComment) => Promise; } - -export interface CaseClientFactoryArguments { - caseConfigureService: CaseConfigureServiceSetup; - caseService: CaseServiceSetup; - userActionService: CaseUserActionServiceSetup; -} 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 0d4180f01d8cb..d346ab573901b 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 @@ -7,6 +7,8 @@ import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; +import { ConnectorTypes } from '../../../../common/api'; +import { CaseClient } from '../../../client'; import { createMockSavedObjectsRepository, createRoute, @@ -14,8 +16,6 @@ import { mockCases, } from '../__fixtures__'; import { initPatchCasesApi } from './patch_cases'; -import { ConnectorTypes } from '../../../../common/api/connectors'; -import { CaseClient } from '../../../client'; describe('PATCH cases', () => { let routeHandler: RequestHandler; @@ -28,29 +28,31 @@ describe('PATCH cases', () => { }); it(`it updates a new case`, async () => { - const patchResult = { - 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, + const patchResult = [ + { + 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' as const, + 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', }, - 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', - }; + ]; const request = httpServerMock.createKibanaRequest({ path: '/api/cases', From c9f58c31f4a1cbb2f1e133347d8b7f2c6ecc4bb4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 26 Oct 2020 17:32:12 +0200 Subject: [PATCH 12/14] Move request to upper level --- .../case/server/client/cases/create.test.ts | 27 +++++++------------ .../case/server/client/cases/create.ts | 4 +-- .../case/server/client/cases/update.test.ts | 21 +++++++-------- .../case/server/client/cases/update.ts | 4 +-- .../client/comments/add_comment.test.ts | 11 -------- .../server/client/comments/add_comment.ts | 2 +- .../plugins/case/server/client/index.test.ts | 6 +++++ x-pack/plugins/case/server/client/index.ts | 18 +++++++++++-- x-pack/plugins/case/server/client/mocks.ts | 3 +++ x-pack/plugins/case/server/client/types.ts | 11 +++----- x-pack/plugins/case/server/plugin.ts | 2 ++ .../api/cases/comments/post_comment.test.ts | 1 - .../routes/api/cases/comments/post_comment.ts | 2 +- .../routes/api/cases/patch_cases.test.ts | 2 +- .../server/routes/api/cases/patch_cases.ts | 2 +- .../server/routes/api/cases/post_case.test.ts | 2 +- .../case/server/routes/api/cases/post_case.ts | 2 +- 17 files changed, 58 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/case/server/client/cases/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts index 5e465ed7fdc9a..f253dd9f4feb4 100644 --- a/x-pack/plugins/case/server/client/cases/create.test.ts +++ b/x-pack/plugins/case/server/client/cases/create.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'kibana/server'; import { ConnectorTypes, CasePostRequest } from '../../../common/api'; import { @@ -14,8 +13,6 @@ import { } from '../../routes/api/__fixtures__'; import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; -const request = {} as KibanaRequest; - describe('create', () => { beforeEach(async () => { jest.restoreAllMocks(); @@ -44,7 +41,7 @@ describe('create', () => { caseConfigureSavedObject: mockCaseConfigure, }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - const res = await caseClient.client.create({ request, theCase: postCase }); + const res = await caseClient.client.create({ theCase: postCase }); expect(res).toEqual({ id: 'mock-it', @@ -115,7 +112,7 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - const res = await caseClient.client.create({ request, theCase: postCase }); + const res = await caseClient.client.create({ theCase: postCase }); expect(res).toEqual({ id: 'mock-it', @@ -154,7 +151,7 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true); - const res = await caseClient.client.create({ request, theCase: postCase }); + const res = await caseClient.client.create({ theCase: postCase }); expect(res).toEqual({ id: 'mock-it', @@ -201,7 +198,7 @@ describe('create', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client // @ts-expect-error - .create({ request, theCase: postCase }) + .create({ theCase: postCase }) .catch((e) => expect(e).not.toBeNull()); }); @@ -224,7 +221,7 @@ describe('create', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client // @ts-expect-error - .create({ request, theCase: postCase }) + .create({ theCase: postCase }) .catch((e) => expect(e).not.toBeNull()); }); @@ -247,7 +244,7 @@ describe('create', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client // @ts-expect-error - .create({ request, theCase: postCase }) + .create({ theCase: postCase }) .catch((e) => expect(e).not.toBeNull()); }); @@ -265,7 +262,7 @@ describe('create', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client // @ts-expect-error - .create({ request, theCase: postCase }) + .create({ theCase: postCase }) .catch((e) => expect(e).not.toBeNull()); }); @@ -289,7 +286,7 @@ describe('create', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client // @ts-expect-error - .create({ request, theCase: postCase }) + .create({ theCase: postCase }) .catch((e) => expect(e).not.toBeNull()); }); @@ -312,9 +309,7 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - caseClient.client - .create({ request, theCase: postCase }) - .catch((e) => expect(e).not.toBeNull()); + caseClient.client.create({ theCase: postCase }).catch((e) => expect(e).not.toBeNull()); }); it(`Returns an error if postNewCase throws`, async () => { @@ -334,9 +329,7 @@ describe('create', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - caseClient.client - .create({ request, theCase: postCase }) - .catch((e) => expect(e).not.toBeNull()); + 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 index 21b32a3f8d8e0..3379099419a75 100644 --- a/x-pack/plugins/case/server/client/cases/create.ts +++ b/x-pack/plugins/case/server/client/cases/create.ts @@ -31,10 +31,8 @@ export const create = ({ caseService, caseConfigureService, userActionService, -}: CaseClientFactoryArguments) => async ({ request, - theCase, -}: CaseClientCreate): Promise => { +}: CaseClientFactoryArguments) => async ({ theCase }: CaseClientCreate): Promise => { const query = pipe( excess(CasePostRequestRt).decode(theCase), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts index 9b9f69d5ed093..62d897999c11a 100644 --- a/x-pack/plugins/case/server/client/cases/update.test.ts +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'kibana/server'; import { ConnectorTypes, CasesPatchRequest } from '../../../common/api'; import { createMockSavedObjectsRepository, @@ -13,8 +12,6 @@ import { } from '../../routes/api/__fixtures__'; import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; -const request = {} as KibanaRequest; - describe('update', () => { beforeEach(async () => { jest.restoreAllMocks(); @@ -41,7 +38,7 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - const res = await caseClient.client.update({ request, cases: patchCases }); + const res = await caseClient.client.update({ cases: patchCases }); expect(res).toEqual([ { @@ -115,7 +112,7 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - const res = await caseClient.client.update({ request, cases: patchCases }); + const res = await caseClient.client.update({ cases: patchCases }); expect(res).toEqual([ { @@ -160,7 +157,7 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - const res = await caseClient.client.update({ request, cases: patchCases }); + const res = await caseClient.client.update({ cases: patchCases }); expect(res).toEqual([ { @@ -210,7 +207,7 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - const res = await caseClient.client.update({ request, cases: patchCases }); + const res = await caseClient.client.update({ cases: patchCases }); expect(res).toEqual([ { @@ -272,7 +269,7 @@ describe('update', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client // @ts-expect-error - .update({ request, cases: patchCases }) + .update({ cases: patchCases }) .catch((e) => expect(e).not.toBeNull()); }); @@ -299,7 +296,7 @@ describe('update', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client // @ts-expect-error - .update({ request, cases: patchCases }) + .update({ cases: patchCases }) .catch((e) => expect(e).not.toBeNull()); }); @@ -321,7 +318,7 @@ describe('update', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client - .update({ request, cases: patchCases }) + .update({ cases: patchCases }) .catch((e) => expect(e.message).toBe('All update fields are identical to current version.') ); @@ -349,7 +346,7 @@ describe('update', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client - .update({ request, cases: patchCases }) + .update({ cases: patchCases }) .catch((e) => expect(e.message).toBe( 'These cases not-exists do not exist. Please check you have the correct ids.' @@ -375,7 +372,7 @@ describe('update', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client - .update({ request, cases: patchCases }) + .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 index ad86bba642b42..424f51ee40f08 100644 --- a/x-pack/plugins/case/server/client/cases/update.ts +++ b/x-pack/plugins/case/server/client/cases/update.ts @@ -32,10 +32,8 @@ export const update = ({ savedObjectsClient, caseService, userActionService, -}: CaseClientFactoryArguments) => async ({ request, - cases, -}: CaseClientUpdate): Promise => { +}: CaseClientFactoryArguments) => async ({ cases }: CaseClientUpdate): Promise => { const query = pipe( excess(CasesPatchRequestRt).decode(cases), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/client/comments/add_comment.test.ts b/x-pack/plugins/case/server/client/comments/add_comment.test.ts index 77e2d6d8afec8..8a316740e41e0 100644 --- a/x-pack/plugins/case/server/client/comments/add_comment.test.ts +++ b/x-pack/plugins/case/server/client/comments/add_comment.test.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'kibana/server'; - import { createMockSavedObjectsRepository, mockCaseComments, @@ -13,8 +11,6 @@ import { } from '../../routes/api/__fixtures__'; import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; -const request = {} as KibanaRequest; - describe('addComment', () => { beforeEach(async () => { jest.restoreAllMocks(); @@ -33,7 +29,6 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); const res = await caseClient.client.addComment({ - request, caseId: 'mock-id-1', comment: { comment: 'Wow, good luck catching that bad meanie!' }, }); @@ -65,7 +60,6 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); const res = await caseClient.client.addComment({ - request, caseId: 'mock-id-1', comment: { comment: 'Wow, good luck catching that bad meanie!' }, }); @@ -86,7 +80,6 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); await caseClient.client.addComment({ - request, caseId: 'mock-id-1', comment: { comment: 'Wow, good luck catching that bad meanie!' }, }); @@ -131,7 +124,6 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true); const res = await caseClient.client.addComment({ - request, caseId: 'mock-id-1', comment: { comment: 'Wow, good luck catching that bad meanie!' }, }); @@ -166,7 +158,6 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client .addComment({ - request, caseId: 'mock-id-1', // @ts-expect-error comment: {}, @@ -188,7 +179,6 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client .addComment({ - request, caseId: 'not-exists', comment: { comment: 'Wow, good luck catching that bad meanie!' }, }) @@ -209,7 +199,6 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client .addComment({ - request, caseId: 'mock-id-1', comment: { comment: 'Throw an error' }, }) diff --git a/x-pack/plugins/case/server/client/comments/add_comment.ts b/x-pack/plugins/case/server/client/comments/add_comment.ts index 8019dfb153e57..765eb2c873765 100644 --- a/x-pack/plugins/case/server/client/comments/add_comment.ts +++ b/x-pack/plugins/case/server/client/comments/add_comment.ts @@ -27,8 +27,8 @@ export const addComment = ({ savedObjectsClient, caseService, userActionService, -}: CaseClientFactoryArguments) => async ({ request, +}: CaseClientFactoryArguments) => async ({ caseId, comment, }: CaseClientAddComment): Promise => { diff --git a/x-pack/plugins/case/server/client/index.test.ts b/x-pack/plugins/case/server/client/index.test.ts index ed75da8b1d522..996205197ae4d 100644 --- a/x-pack/plugins/case/server/client/index.test.ts +++ b/x-pack/plugins/case/server/client/index.test.ts @@ -4,6 +4,7 @@ * 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 { @@ -24,6 +25,7 @@ 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; @@ -33,6 +35,7 @@ describe('createCaseClient()', () => { test('it creates the client correctly', async () => { createCaseClient({ savedObjectsClient, + request, caseConfigureService, caseService, userActionService, @@ -40,6 +43,7 @@ describe('createCaseClient()', () => { expect(createMock).toHaveBeenCalledWith({ savedObjectsClient, + request, caseConfigureService, caseService, userActionService, @@ -47,6 +51,7 @@ describe('createCaseClient()', () => { expect(updateMock).toHaveBeenCalledWith({ savedObjectsClient, + request, caseConfigureService, caseService, userActionService, @@ -54,6 +59,7 @@ describe('createCaseClient()', () => { 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 index ad238729cbd92..9dac9716b6f22 100644 --- a/x-pack/plugins/case/server/client/index.ts +++ b/x-pack/plugins/case/server/client/index.ts @@ -13,15 +13,29 @@ export { CaseClient } from './types'; export const createCaseClient = ({ savedObjectsClient, + request, caseConfigureService, caseService, userActionService, }: CaseClientFactoryArguments): CaseClient => { return { - create: create({ savedObjectsClient, caseConfigureService, caseService, userActionService }), - update: update({ savedObjectsClient, caseConfigureService, caseService, userActionService }), + 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 index ad2815269bea9..243dd884f9ef6 100644 --- a/x-pack/plugins/case/server/client/mocks.ts +++ b/x-pack/plugins/case/server/client/mocks.ts @@ -4,6 +4,7 @@ * 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'; @@ -25,6 +26,7 @@ export const createCaseClientWithMockSavedObjectsClient = async ( services: { userActionService: jest.Mocked }; }> => { const log = loggingSystemMock.create().get('case'); + const request = {} as KibanaRequest; const caseServicePlugin = new CaseService(log); const caseConfigureServicePlugin = new CaseConfigureService(log); @@ -41,6 +43,7 @@ export const createCaseClientWithMockSavedObjectsClient = async ( return { client: createCaseClient({ savedObjectsClient, + request, caseService, caseConfigureService, userActionService, diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index ef1dd94dd50c2..8db7d8a5747d7 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -18,25 +18,22 @@ import { CaseUserActionServiceSetup, } from '../services'; -export interface CaseClientFunctionArguments { - request: KibanaRequest; -} - -export interface CaseClientCreate extends CaseClientFunctionArguments { +export interface CaseClientCreate { theCase: CasePostRequest; } -export interface CaseClientUpdate extends CaseClientFunctionArguments { +export interface CaseClientUpdate { cases: CasesPatchRequest; } -export interface CaseClientAddComment extends CaseClientFunctionArguments { +export interface CaseClientAddComment { caseId: string; comment: CommentRequest; } export interface CaseClientFactoryArguments { savedObjectsClient: SavedObjectsClientContract; + request: KibanaRequest; caseConfigureService: CaseConfigureServiceSetup; caseService: CaseServiceSetup; userActionService: CaseUserActionServiceSetup; diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 3b2032143ece4..5398f8ed0ae83 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -102,6 +102,7 @@ export class CasePlugin { const getCaseClientWithRequest = async (request: KibanaRequest) => { return createCaseClient({ savedObjectsClient: core.savedObjects.getScopedClient(request), + request, caseService: this.caseService!, caseConfigureService: this.caseConfigureService!, userActionService: this.userActionService!, @@ -137,6 +138,7 @@ export class CasePlugin { caseService, caseConfigureService, userActionService, + request, }); }, }; 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 0c81aaf6ae11f..cd2adc415c521 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 @@ -120,7 +120,6 @@ describe('POST comment', () => { expect(caseClient.addComment).toHaveBeenCalledTimes(1); expect(caseClient.addComment).toHaveBeenCalledWith({ - request, caseId: request.params.case_id, comment: request.body, }); 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 bcf688c9d2478..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 @@ -32,7 +32,7 @@ export function initPostCommentApi({ router }: RouteDeps) { try { return response.ok({ - body: await caseClient.addComment({ request, caseId, comment }), + body: await caseClient.addComment({ caseId, comment }), }); } catch (error) { return response.customError(wrapError(error)); 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 d346ab573901b..6a9b1936d1767 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 @@ -79,7 +79,7 @@ describe('PATCH cases', () => { const response = await routeHandler(context, request, kibanaResponseFactory); expect(caseClient.update).toHaveBeenCalledTimes(1); - expect(caseClient.update).toHaveBeenCalledWith({ request, cases: request.body }); + expect(caseClient.update).toHaveBeenCalledWith({ cases: request.body }); expect(response.status).toEqual(200); expect(response.payload).toEqual(patchResult); }); 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 8e0dd85101cef..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 @@ -27,7 +27,7 @@ export function initPatchCasesApi({ router }: RouteDeps) { try { return response.ok({ - body: await caseClient.update({ request, cases }), + body: await caseClient.update({ cases }), }); } 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 3d80be79308f9..bbb4feac3a4d6 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 @@ -80,7 +80,7 @@ describe('POST cases', () => { const response = await routeHandler(context, request, kibanaResponseFactory); expect(caseClient.create).toHaveBeenCalledTimes(1); - expect(caseClient.create).toHaveBeenCalledWith({ request, theCase: request.body }); + expect(caseClient.create).toHaveBeenCalledWith({ theCase: request.body }); expect(response.status).toEqual(200); expect(response.payload).toEqual(createResult); }); 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 8d08c92add39d..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 @@ -27,7 +27,7 @@ export function initPostCaseApi({ router }: RouteDeps) { try { return response.ok({ - body: await caseClient.create({ request, theCase }), + body: await caseClient.create({ theCase }), }); } catch (error) { return response.customError(wrapError(error)); From 79a36474ec15c87198567c072d3df048ddf7aba2 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 27 Oct 2020 18:20:44 +0200 Subject: [PATCH 13/14] Revert old tests --- .../routes/api/__fixtures__/route_contexts.ts | 33 ++- .../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 | 172 +++++++------ .../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 | 240 ++++++++++++++++-- .../server/routes/api/cases/post_case.test.ts | 182 ++++++++++--- .../server/routes/api/cases/push_case.test.ts | 6 +- 15 files changed, 548 insertions(+), 185 deletions(-) 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 f2b2784c427d1..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,15 +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 { createCaseClientMock } from '../../../client/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 caseMock = createCaseClientMock(); + 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: { @@ -21,6 +42,8 @@ export const createRouteContext = (client: any) => { }, }, actions: { getActionsClient: () => actionsMock }, - case: { getCaseClient: () => caseMock }, + 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 cd2adc415c521..5c902fcdac9fb 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 @@ -14,10 +14,8 @@ import { mockCases, mockCaseComments, } from '../../__fixtures__'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; -import { CaseClient } from '../../../../client'; import { initPostCommentApi } from './post_comment'; -import { ConnectorTypes } from '../../../../../common/api'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; describe('POST comment', () => { let routeHandler: RequestHandler; @@ -25,77 +23,86 @@ describe('POST comment', () => { routeHandler = await createRoute(initPostCommentApi, 'post'); const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2020-10-23T21:54:48.952Z'), + toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), })); }); - it(`it adds a new comment`, async () => { - const addCommentResult = { - id: 'mock-id-1', - version: 'WzE3LDFd', - comments: [ - { - id: 'mock-comment-1', - version: 'WzEsMV0=', - comment: 'Wow, good luck catching that bad meanie!', - created_at: '2019-11-25T21:55:00.177Z', - created_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - pushed_at: null, - pushed_by: null, - updated_at: '2019-11-25T21:55:00.177Z', - updated_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - }, - { - id: 'mock-comment', - version: 'WzksMV0=', - 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', - }, - pushed_at: null, - pushed_by: null, - updated_at: null, - updated_by: null, - }, - ], - totalComment: 2, - closed_at: null, - closed_by: null, - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, + it(`Posts a new comment`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'post', + params: { + case_id: 'mock-id-1', }, - created_at: '2019-11-25T21:54:48.952Z', - created_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', + body: { + comment: 'Wow, good luck catching that bad meanie!', + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.comments[response.payload.comments.length - 1].id).toEqual( + 'mock-comment' + ); + }); + + it(`Returns an error if the case does not exist`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'post', + params: { + case_id: 'this-is-not-real', + }, + body: { + comment: 'Wow, good luck catching that bad meanie!', + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + 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, + method: 'post', + params: { + case_id: 'mock-id-1', }, - description: 'This is a brand new case of a bad meanie defacing data', - external_service: null, - title: 'Super Bad Security Issue', - status: 'open' as const, - tags: ['defacement'], - updated_at: '2020-10-23T21:54:48.952Z', - updated_by: { - username: 'awesome', - full_name: 'Awesome D00d', - email: 'd00d@awesome.com', + body: { + comment: 'Throw an error', }, - }; + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + 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); const request = httpServerMock.createKibanaRequest({ path: CASE_COMMENTS_URL, @@ -108,22 +115,29 @@ describe('POST comment', () => { }, }); - const context = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const caseClient = context.case!.getCaseClient() as jest.Mocked; - caseClient.addComment.mockResolvedValueOnce(addCommentResult); - const response = await routeHandler(context, request, kibanaResponseFactory); - - expect(caseClient.addComment).toHaveBeenCalledTimes(1); - expect(caseClient.addComment).toHaveBeenCalledWith({ - caseId: request.params.case_id, - comment: request.body, - }); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload).toEqual(addCommentResult); + expect(response.payload.comments[response.payload.comments.length - 1]).toEqual({ + comment: 'Wow, good luck catching that bad meanie!', + created_at: '2019-11-25T21: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=', + }); }); }); 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 6a9b1936d1767..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 @@ -7,15 +7,16 @@ import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; -import { ConnectorTypes } from '../../../../common/api'; -import { CaseClient } from '../../../client'; import { createMockSavedObjectsRepository, createRoute, createRouteContext, mockCases, + mockCaseComments, } from '../__fixtures__'; import { initPatchCasesApi } from './patch_cases'; +import { mockCaseConfigure, mockCaseNoConnectorId } from '../__fixtures__/mock_saved_objects'; +import { ConnectorTypes } from '../../../../common/api/connectors'; describe('PATCH cases', () => { let routeHandler: RequestHandler; @@ -27,8 +28,30 @@ describe('PATCH cases', () => { })); }); - it(`it updates a new case`, async () => { - const patchResult = [ + it(`Close a case`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases', + method: 'patch', + body: { + cases: [ + { + id: 'mock-id-1', + status: 'closed', + version: 'WzAsMV0=', + }, + ], + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload).toEqual([ { closed_at: '2019-11-25T21:54:48.952Z', closed_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, @@ -44,7 +67,7 @@ describe('PATCH cases', () => { description: 'This is a brand new case of a bad meanie defacing data', id: 'mock-id-1', external_service: null, - status: 'closed' as const, + status: 'closed', tags: ['defacement'], title: 'Super Bad Security Issue', totalComment: 0, @@ -52,35 +75,222 @@ describe('PATCH cases', () => { updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, version: 'WzE3LDFd', }, - ]; + ]); + }); + it(`Open a case`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases', method: 'patch', body: { cases: [ { - id: 'mock-id-1', - status: 'closed' as const, + id: 'mock-id-4', + status: 'open', + version: 'WzUsMV0=', + }, + ], + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload).toEqual([ + { + closed_at: null, + closed_by: null, + comments: [], + connector: { + id: '123', + name: 'My connector', + type: '.jira', + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + created_at: '2019-11-25T22:32:17.947Z', + created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' }, + description: 'Oh no, a bad meanie going LOLBins all over the place!', + id: 'mock-id-4', + external_service: null, + status: 'open', + tags: ['LOLBins'], + title: 'Another bad one', + totalComment: 0, + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + version: 'WzE3LDFd', + }, + ]); + }); + + it(`Patches a case without a connector.id`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases', + method: 'patch', + body: { + cases: [ + { + id: 'mock-no-connector_id', + status: 'closed', version: 'WzAsMV0=', }, ], }, }); - const context = createRouteContext( + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: [mockCaseNoConnectorId], + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload[0].connector.id).toEqual('none'); + }); + + it(`Patches a case with a connector.id`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases', + method: 'patch', + body: { + cases: [ + { + id: 'mock-id-3', + status: 'closed', + version: 'WzUsMV0=', + }, + ], + }, + }); + + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const caseClient = context.case!.getCaseClient() as jest.Mocked; - caseClient.update.mockResolvedValueOnce(patchResult); - const response = await routeHandler(context, request, kibanaResponseFactory); + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload[0].connector.id).toEqual('123'); + }); + + it(`Change connector`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases', + method: 'patch', + body: { + cases: [ + { + id: 'mock-id-3', + connector: { + id: '456', + name: 'My connector 2', + type: '.jira', + fields: { issueType: 'Bug', priority: 'Low', parent: null }, + }, + version: 'WzUsMV0=', + }, + ], + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }) + ); - expect(caseClient.update).toHaveBeenCalledTimes(1); - expect(caseClient.update).toHaveBeenCalledWith({ cases: request.body }); + const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload).toEqual(patchResult); + expect(response.payload[0].connector).toEqual({ + id: '456', + name: 'My connector 2', + type: '.jira', + fields: { issueType: 'Bug', priority: 'Low', parent: null }, + }); + }); + + it(`Fails with 409 if version does not match`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases', + method: 'patch', + body: { + cases: [ + { + id: 'mock-id-1', + case: { status: 'closed' }, + version: 'badv=', + }, + ], + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(409); + }); + + it(`Fails with 406 if updated field is unchanged`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases', + method: 'patch', + body: { + cases: [ + { + id: 'mock-id-1', + case: { status: 'open' }, + version: 'WzAsMV0=', + }, + ], + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(406); + }); + + it(`Returns an error if updateCase throws`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases', + method: 'patch', + body: { + cases: [ + { + id: 'mock-id-does-not-exist', + status: 'closed', + version: 'WzAsMV0=', + }, + ], + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(404); + expect(response.payload.isBoom).toEqual(true); }); }); 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 bbb4feac3a4d6..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 @@ -15,8 +15,8 @@ import { } from '../__fixtures__'; import { initPostCaseApi } from './post_case'; import { CASES_URL } from '../../../../common/constants'; +import { mockCaseConfigure } from '../__fixtures__/mock_saved_objects'; import { ConnectorTypes } from '../../../../common/api/connectors'; -import { CaseClient } from '../../../client'; describe('POST cases', () => { let routeHandler: RequestHandler; @@ -28,31 +28,7 @@ describe('POST cases', () => { })); }); - it(`it creates a new case`, async () => { - const createResult = { - 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' as const, - tags: ['defacement'], - updated_at: null, - updated_by: null, - version: 'WzksMV0=', - }; - + it(`Posts a new case, no connector configured`, async () => { const request = httpServerMock.createKibanaRequest({ path: CASES_URL, method: 'post', @@ -69,19 +45,159 @@ describe('POST cases', () => { }, }); - const context = createRouteContext( + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.id).toEqual('mock-it'); + expect(response.payload.created_by.username).toEqual('awesome'); + expect(response.payload.connector).toEqual({ + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }); + }); + + it(`Posts a new case, connector provided`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: CASES_URL, + method: 'post', + body: { + 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 }, + }, + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseConfigureSavedObject: mockCaseConfigure, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.connector).toEqual({ + id: '123', + name: 'Jira', + type: '.jira', + fields: { issueType: 'Task', priority: 'High', parent: null }, + }); + }); + + it(`Error if you passing status for a new case`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: CASES_URL, + method: 'post', + body: { + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + status: 'open', + tags: ['defacement'], + connector: null, + }, + }); + + const theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + }); + + it(`Returns an error if postNewCase throws`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: CASES_URL, + method: 'post', + body: { + description: 'Throw an error', + title: 'Super Bad Security Issue', + tags: ['error'], + connector: null, + }, + }); + + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const caseClient = context.case!.getCaseClient() as jest.Mocked; - caseClient.create.mockResolvedValueOnce(createResult); - const response = await routeHandler(context, request, kibanaResponseFactory); + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + expect(response.payload.isBoom).toEqual(true); + }); + + it(`Allow user to create case without authentication`, async () => { + routeHandler = await createRoute(initPostCaseApi, 'post', true); - expect(caseClient.create).toHaveBeenCalledTimes(1); - expect(caseClient.create).toHaveBeenCalledWith({ theCase: request.body }); + const request = httpServerMock.createKibanaRequest({ + path: CASES_URL, + method: 'post', + body: { + 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 theContext = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseConfigureSavedObject: mockCaseConfigure, + }), + true + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload).toEqual(createResult); + expect(response.payload).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: null, + full_name: null, + username: null, + }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + id: 'mock-it', + status: 'open', + tags: ['defacement'], + title: 'Super Bad Security Issue', + totalComment: 0, + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); }); }); 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, }) From 6b46eb732474bae408ab253afaa1df4baad8589b Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 27 Oct 2020 18:34:13 +0200 Subject: [PATCH 14/14] Rename action --- .../client/comments/{add_comment.test.ts => add.test.ts} | 0 .../case/server/client/comments/{add_comment.ts => add.ts} | 0 x-pack/plugins/case/server/client/index.test.ts | 4 ++-- x-pack/plugins/case/server/client/index.ts | 2 +- .../server/routes/api/cases/comments/post_comment.test.ts | 3 ++- 5 files changed, 5 insertions(+), 4 deletions(-) rename x-pack/plugins/case/server/client/comments/{add_comment.test.ts => add.test.ts} (100%) rename x-pack/plugins/case/server/client/comments/{add_comment.ts => add.ts} (100%) diff --git a/x-pack/plugins/case/server/client/comments/add_comment.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts similarity index 100% rename from x-pack/plugins/case/server/client/comments/add_comment.test.ts rename to x-pack/plugins/case/server/client/comments/add.test.ts diff --git a/x-pack/plugins/case/server/client/comments/add_comment.ts b/x-pack/plugins/case/server/client/comments/add.ts similarity index 100% rename from x-pack/plugins/case/server/client/comments/add_comment.ts rename to x-pack/plugins/case/server/client/comments/add.ts diff --git a/x-pack/plugins/case/server/client/index.test.ts b/x-pack/plugins/case/server/client/index.test.ts index 996205197ae4d..1ecdc8ea96dea 100644 --- a/x-pack/plugins/case/server/client/index.test.ts +++ b/x-pack/plugins/case/server/client/index.test.ts @@ -15,11 +15,11 @@ import { import { create } from './cases/create'; import { update } from './cases/update'; -import { addComment } from './comments/add_comment'; +import { addComment } from './comments/add'; jest.mock('./cases/create'); jest.mock('./cases/update'); -jest.mock('./comments/add_comment'); +jest.mock('./comments/add'); const caseService = createCaseServiceMock(); const caseConfigureService = createConfigureServiceMock(); diff --git a/x-pack/plugins/case/server/client/index.ts b/x-pack/plugins/case/server/client/index.ts index 9dac9716b6f22..75e9e3c4cfebc 100644 --- a/x-pack/plugins/case/server/client/index.ts +++ b/x-pack/plugins/case/server/client/index.ts @@ -7,7 +7,7 @@ import { CaseClientFactoryArguments, CaseClient } from './types'; import { create } from './cases/create'; import { update } from './cases/update'; -import { addComment } from './comments/add_comment'; +import { addComment } from './comments/add'; export { CaseClient } from './types'; 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 5c902fcdac9fb..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 @@ -119,7 +119,8 @@ describe('POST comment', () => { createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, - }) + }), + true ); const response = await routeHandler(theContext, request, kibanaResponseFactory);