diff --git a/x-pack/platform/plugins/shared/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/all_cases/all_cases_list.test.tsx index 5c79aadbcfeeb..7640276838de9 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/all_cases/all_cases_list.test.tsx @@ -90,6 +90,44 @@ const mockKibana = () => { } as unknown as ReturnType); }; +// eslint-disable-next-line prefer-object-spread +const originalGetComputedStyle = Object.assign({}, window.getComputedStyle); + +const restoreGetComputedStyle = () => { + Object.defineProperty(window, 'getComputedStyle', originalGetComputedStyle); +}; + +const patchGetComputedStyle = () => { + // The JSDOM implementation is too slow + // Especially for dropdowns that try to position themselves + // perf issue - https://github.com/jsdom/jsdom/issues/3234 + Object.defineProperty(window, 'getComputedStyle', { + value: (el: HTMLElement) => { + /** + * This is based on the jsdom implementation of getComputedStyle + * https://github.com/jsdom/jsdom/blob/9dae17bf0ad09042cfccd82e6a9d06d3a615d9f4/lib/jsdom/browser/Window.js#L779-L820 + * + * It is missing global style parsing and will only return styles applied directly to an element. + * Will not return styles that are global or from emotion + */ + const declaration = new CSSStyleDeclaration(); + const { style } = el; + + Array.prototype.forEach.call(style, (property: string) => { + declaration.setProperty( + property, + style.getPropertyValue(property), + style.getPropertyPriority(property) + ); + }); + + return declaration; + }, + configurable: true, + writable: true, + }); +}; + // FLAKY: https://github.com/elastic/kibana/issues/192739 describe.skip('AllCasesListGeneric', () => { const onRowClick = jest.fn(); @@ -113,48 +151,18 @@ describe.skip('AllCasesListGeneric', () => { }; const removeMsFromDate = (value: string) => moment(value).format('YYYY-MM-DDTHH:mm:ss[Z]'); - // eslint-disable-next-line prefer-object-spread - const originalGetComputedStyle = Object.assign({}, window.getComputedStyle); let appMockRenderer: AppMockRenderer; beforeAll(() => { - // The JSDOM implementation is too slow - // Especially for dropdowns that try to position themselves - // perf issue - https://github.com/jsdom/jsdom/issues/3234 - Object.defineProperty(window, 'getComputedStyle', { - value: (el: HTMLElement) => { - /** - * This is based on the jsdom implementation of getComputedStyle - * https://github.com/jsdom/jsdom/blob/9dae17bf0ad09042cfccd82e6a9d06d3a615d9f4/lib/jsdom/browser/Window.js#L779-L820 - * - * It is missing global style parsing and will only return styles applied directly to an element. - * Will not return styles that are global or from emotion - */ - const declaration = new CSSStyleDeclaration(); - const { style } = el; - - Array.prototype.forEach.call(style, (property: string) => { - declaration.setProperty( - property, - style.getPropertyValue(property), - style.getPropertyPriority(property) - ); - }); - - return declaration; - }, - configurable: true, - writable: true, - }); - + patchGetComputedStyle(); mockKibana(); const actionTypeRegistry = useKibanaMock().services.triggersActionsUi.actionTypeRegistry; registerConnectorsToMockActionRegistry(actionTypeRegistry, connectorsMock); }); afterAll(() => { - Object.defineProperty(window, 'getComputedStyle', originalGetComputedStyle); + restoreGetComputedStyle(); }); beforeEach(() => { diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.test.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.test.ts index ad7550ee857af..656bc699fefff 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.test.ts @@ -66,6 +66,15 @@ describe('User profiles API', () => { expect(res).toEqual(userProfiles); }); + it('should filter out empty user profiles', async () => { + const res = await bulkGetUserProfiles({ + security, + uids: [...userProfilesIds, ''], + }); + + expect(res).toEqual(userProfiles); + }); + it('calls bulkGet correctly', async () => { await bulkGetUserProfiles({ security, diff --git a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.ts b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.ts index 581e808dfb07d..79bece053bb37 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/user_profiles/api.ts @@ -8,6 +8,7 @@ import type { HttpStart } from '@kbn/core/public'; import type { UserProfile } from '@kbn/security-plugin/common'; import type { SecurityPluginStart } from '@kbn/security-plugin/public'; +import { isEmpty } from 'lodash'; import { INTERNAL_SUGGEST_USER_PROFILES_URL, DEFAULT_USER_SIZE } from '../../../common/constants'; export interface SuggestUserProfilesArgs { @@ -42,11 +43,12 @@ export const bulkGetUserProfiles = async ({ security, uids, }: BulkGetUserProfilesArgs): Promise => { - if (uids.length === 0) { + const cleanUids: string[] = uids.filter((uid) => !isEmpty(uid)); + if (cleanUids.length === 0) { return []; } - return security.userProfiles.bulkGet({ uids: new Set(uids), dataPath: 'avatar' }); + return security.userProfiles.bulkGet({ uids: new Set(cleanUids), dataPath: 'avatar' }); }; export interface GetCurrentUserProfileArgs { diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts index 9e80cf5752006..affacdf67f8b8 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts @@ -399,6 +399,29 @@ describe('update', () => { Operations.updateCase, ]); }); + + it('should filter out empty user profiles', async () => { + const casesWithEmptyAssignee = { + cases: [ + { + ...cases.cases[0], + assignees: [{ uid: '' }, { uid: '2' }], + }, + ], + }; + await bulkUpdate(casesWithEmptyAssignee, clientArgs, casesClientMock); + expect(clientArgs.services.caseService.patchCases).toHaveBeenCalledWith( + expect.objectContaining({ + cases: expect.arrayContaining([ + expect.objectContaining({ + updatedAttributes: expect.objectContaining({ + assignees: [{ uid: '2' }], + }), + }), + ]), + }) + ); + }); }); describe('Category', () => { diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts index d714d7f22e83c..f6e965d484913 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts @@ -61,6 +61,7 @@ import type { import { CasesPatchRequestRt } from '../../../common/types/api'; import { CasesRt, CaseStatuses, AttachmentType } from '../../../common/types/domain'; import { validateCustomFields } from './validators'; +import { emptyCasesAssigneesSanitizer } from './sanitizers'; /** * Throws an error if any of the requests attempt to update the owner of a case. @@ -384,7 +385,8 @@ export const bulkUpdate = async ( } = clientArgs; try { - const query = decodeWithExcessOrThrow(CasesPatchRequestRt)(cases); + const rawQuery = decodeWithExcessOrThrow(CasesPatchRequestRt)(cases); + const query = emptyCasesAssigneesSanitizer(rawQuery); const caseIds = query.cases.map((q) => q.id); const myCases = await caseService.getCases({ caseIds, diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/create.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/create.test.ts index 9bc5455a0633d..5392c5399aff3 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/create.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/create.test.ts @@ -161,6 +161,22 @@ describe('create', () => { }) ); }); + + it('should filter out empty assignees', async () => { + await create( + { ...theCase, assignees: [{ uid: '' }, { uid: '1' }] }, + clientArgs, + casesClientMock + ); + + expect(clientArgs.services.caseService.createCase).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + assignees: [{ uid: '1' }], + }), + }) + ); + }); }); describe('Attributes', () => { diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/create.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/create.ts index 88dafdb4e9dc3..8e12df1381a72 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/create.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/create.ts @@ -21,6 +21,7 @@ import type { CasePostRequest } from '../../../common/types/api'; import { CasePostRequestRt } from '../../../common/types/api'; import {} from '../utils'; import { validateCustomFields } from './validators'; +import { emptyCaseAssigneesSanitizer } from './sanitizers'; import { normalizeCreateCaseRequest } from './utils'; /** @@ -40,7 +41,8 @@ export const create = async ( } = clientArgs; try { - const query = decodeWithExcessOrThrow(CasePostRequestRt)(data); + const rawQuery = decodeWithExcessOrThrow(CasePostRequestRt)(data); + const query = emptyCaseAssigneesSanitizer(rawQuery); const configurations = await casesClient.configure.get({ owner: data.owner }); const customFieldsConfiguration = configurations[0]?.customFields; diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/sanitizers.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/sanitizers.ts new file mode 100644 index 0000000000000..ec548a53e223c --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/sanitizers.ts @@ -0,0 +1,34 @@ +/* + * 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 { isEmpty } from 'lodash'; +import type { CaseUserProfile } from '../../../common/types/domain/user/v1'; + +export const emptyCaseAssigneesSanitizer = ( + theCase: T +): T => { + if (isEmpty(theCase.assignees)) { + return theCase; + } + + return { + ...theCase, + assignees: theCase.assignees?.filter(({ uid }) => !isEmpty(uid)), + }; +}; + +export const emptyCasesAssigneesSanitizer = ({ + cases, +}: { + cases: T[]; +}): { cases: T[] } => { + return { + cases: cases.map((theCase) => { + return emptyCaseAssigneesSanitizer(theCase); + }), + }; +}; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/patch_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/patch_case.ts new file mode 100644 index 0000000000000..f33cfe44718cd --- /dev/null +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/patch_case.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { postCaseReq, postCaseResp } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + removeServerGeneratedPropertiesFromCase, + updateCase, +} from '../../../../common/lib/api'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +export const defaultUser = { email: null, full_name: null, username: 'elastic' }; +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('patch_case', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should filter out empty assignee.uid values', async () => { + const randomUid = '7f3e9d2a-1b8c-4c5f-9e6d-8f2a4b1d3c7e'; + const postedCase = await createCase(supertest, postCaseReq); + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + assignees: [{ uid: '' }, { uid: randomUid }], + }, + ], + }, + }); + + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + expect(data).to.eql({ + ...postCaseResp(), + assignees: [{ uid: randomUid }], + updated_by: defaultUser, + }); + }); + }); +}; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/post_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/post_case.ts new file mode 100644 index 0000000000000..2f846420dc291 --- /dev/null +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/post_case.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + removeServerGeneratedPropertiesFromCase, +} from '../../../../common/lib/api'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('post_case', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should filter out empty assignee.uid values', async () => { + const randomUid = '7f3e9d2a-1b8c-4c5f-9e6d-8f2a4b1d3c7e'; + const createdCase = await createCase( + supertest, + getPostCaseRequest({ + assignees: [{ uid: '' }, { uid: randomUid }], + }) + ); + + const data = removeServerGeneratedPropertiesFromCase(createdCase); + + expect(data.assignees).to.eql([{ uid: randomUid }]); + }); + }); +}; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts index 2b3c282ad6c2b..ed06774ca0c32 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts @@ -32,6 +32,8 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { loadTestFile(require.resolve('./cases/user_actions/find_user_actions')); loadTestFile(require.resolve('./cases/assignees')); loadTestFile(require.resolve('./cases/find_cases')); + loadTestFile(require.resolve('./cases/post_case')); + loadTestFile(require.resolve('./cases/patch_case')); loadTestFile(require.resolve('./configure')); loadTestFile(require.resolve('./attachments_framework/registered_persistable_state_trial')); // sub privileges are only available with a license above basic