Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cases] Notify assignees when assigned to a case #144391

Merged
merged 15 commits into from
Nov 8, 2022
3 changes: 2 additions & 1 deletion x-pack/plugins/cases/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"triggersActionsUi",
"management",
"spaces",
"security"
"security",
"notifications"
],
"requiredBundles": [
"savedObjects"
Expand Down
21 changes: 19 additions & 2 deletions x-pack/plugins/cases/server/attachment_framework/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ import type {
CommentRequestPersistableStateType,
} from '../../common/api';
import { ExternalReferenceStorageType } from '../../common/api';
import { ExternalReferenceAttachmentTypeRegistry } from './external_reference_registry';
import { PersistableStateAttachmentTypeRegistry } from './persistable_state_registry';
import type { PersistableStateAttachmentTypeSetup, PersistableStateAttachmentState } from './types';
import type {
PersistableStateAttachmentTypeSetup,
PersistableStateAttachmentState,
ExternalReferenceAttachmentType,
} from './types';

export const getPersistableAttachment = (): PersistableStateAttachmentTypeSetup => ({
id: '.test',
Expand All @@ -42,6 +47,10 @@ export const getPersistableAttachment = (): PersistableStateAttachmentTypeSetup
}),
});

export const getExternalReferenceAttachment = (): ExternalReferenceAttachmentType => ({
id: '.test',
});

export const externalReferenceAttachmentSO = {
type: CommentType.externalReference as const,
externalReferenceId: 'my-id',
Expand Down Expand Up @@ -130,10 +139,18 @@ export const externalReferenceAttachmentSOAttributesWithoutRefs = omit(
'externalReferenceId'
);

export const getPersistableStateAttachmentTypeRegistry =
export const createPersistableStateAttachmentTypeRegistryMock =
(): PersistableStateAttachmentTypeRegistry => {
const persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry();
persistableStateAttachmentTypeRegistry.register(getPersistableAttachment());

return persistableStateAttachmentTypeRegistry;
};

export const createExternalReferenceAttachmentTypeRegistryMock =
(): ExternalReferenceAttachmentTypeRegistry => {
const externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry();
externalReferenceAttachmentTypeRegistry.register(getExternalReferenceAttachment());

return externalReferenceAttachmentTypeRegistry;
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { CommentType, SECURITY_SOLUTION_OWNER } from '../../common';
import {
getPersistableStateAttachmentTypeRegistry,
createPersistableStateAttachmentTypeRegistryMock,
persistableStateAttachment,
persistableStateAttachmentAttributes,
} from './mocks';
Expand All @@ -19,7 +19,7 @@ import {
} from './so_references';

describe('Persistable state SO references', () => {
const persistableStateAttachmentTypeRegistry = getPersistableStateAttachmentTypeRegistry();
const persistableStateAttachmentTypeRegistry = createPersistableStateAttachmentTypeRegistryMock();
const references = [
{
id: 'testRef',
Expand Down
80 changes: 80 additions & 0 deletions x-pack/plugins/cases/server/client/cases/create.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { SECURITY_SOLUTION_OWNER } from '../../../common';
import { CaseSeverity, ConnectorTypes } from '../../../common/api';
import { mockCases } from '../../mocks';
import { createCasesClientMockArgs } from '../mocks';
import { create } from './create';

describe('create', () => {
const theCase = {
title: 'My Case',
tags: [],
description: 'testing sir',
connector: {
id: '.none',
name: 'None',
type: ConnectorTypes.none,
fields: null,
},
settings: { syncAlerts: true },
severity: CaseSeverity.LOW,
owner: SECURITY_SOLUTION_OWNER,
assignees: [{ uid: '1' }],
};

const caseSO = mockCases[0];

describe('Assignees', () => {
const clientArgs = createCasesClientMockArgs();
clientArgs.services.caseService.postNewCase.mockResolvedValue(caseSO);

beforeEach(() => {
jest.clearAllMocks();
});

it('notifies single assignees', async () => {
await create(theCase, clientArgs);

expect(clientArgs.services.notificationService.notifyAssignees).toHaveBeenCalledWith({
assignees: theCase.assignees,
theCase: caseSO,
});
});

it('notifies multiple assignees', async () => {
await create({ ...theCase, assignees: [{ uid: '1' }, { uid: '2' }] }, clientArgs);

expect(clientArgs.services.notificationService.notifyAssignees).toHaveBeenCalledWith({
assignees: [{ uid: '1' }, { uid: '2' }],
theCase: caseSO,
});
});

it('does not notify when there are no assignees', async () => {
await create({ ...theCase, assignees: [] }, clientArgs);

expect(clientArgs.services.notificationService.notifyAssignees).not.toHaveBeenCalled();
});

it('does not notify the current user', async () => {
await create(
{
...theCase,
assignees: [{ uid: '1' }, { uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }],
},
clientArgs
);

expect(clientArgs.services.notificationService.notifyAssignees).toHaveBeenCalledWith({
assignees: [{ uid: '1' }],
theCase: caseSO,
});
});
});
});
23 changes: 17 additions & 6 deletions x-pack/plugins/cases/server/client/cases/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const create = async (
): Promise<CaseResponse> => {
const {
unsecuredSavedObjectsClient,
services: { caseService, userActionService, licensingService },
services: { caseService, userActionService, licensingService, notificationService },
user,
logger,
authorization: auth,
Expand Down Expand Up @@ -116,11 +116,22 @@ export const create = async (
owner: newCase.attributes.owner,
});

return CaseResponseRt.encode(
flattenCaseSavedObject({
savedObject: newCase,
})
);
const flattenedCase = flattenCaseSavedObject({
savedObject: newCase,
});

if (query.assignees && query.assignees.length !== 0) {
const assigneesWithoutCurrentUser = query.assignees.filter(
(assignee) => assignee.uid !== user.profile_uid
);

await notificationService.notifyAssignees({
assignees: assigneesWithoutCurrentUser,
theCase: newCase,
});
}

return CaseResponseRt.encode(flattenedCase);
} catch (error) {
throw createCaseError({ message: `Failed to create case: ${error}`, error, logger });
}
Expand Down
5 changes: 3 additions & 2 deletions x-pack/plugins/cases/server/client/cases/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 type { SavedObject, SavedObjectsResolveResponse } from '@kbn/core/server';
import type { SavedObjectsResolveResponse } from '@kbn/core/server';
import type {
CaseResponse,
CaseResolveResponse,
Expand Down Expand Up @@ -37,6 +37,7 @@ import type { CasesClientArgs } from '..';
import { Operations } from '../../authorization';
import { combineAuthorizedAndOwnerFilter } from '../utils';
import { CasesService } from '../../services';
import type { CaseSavedObject } from '../../common/types';

/**
* Parameters for finding cases IDs using an alert ID
Expand Down Expand Up @@ -182,7 +183,7 @@ export const get = async (
} = clientArgs;

try {
const theCase: SavedObject<CaseAttributes> = await caseService.getCase({
const theCase: CaseSavedObject = await caseService.getCase({
id,
});

Expand Down
Loading