diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 9e94e30cf063a..7728ecb54d766 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -53,6 +53,13 @@ window.scrollTo = jest.fn(); export const mockedFilesClient = createMockFilesClient() as unknown as ScopedFilesClient; +// @ts-ignore +mockedFilesClient.getFileKind.mockImplementation(() => ({ + id: 'test', + maxSizeBytes: 10000, + http: {}, +})); + const mockGetFilesClient = () => mockedFilesClient; export const mockedTestProvidersOwner = [SECURITY_SOLUTION_OWNER]; @@ -141,7 +148,7 @@ export const testQueryClient = new QueryClient({ export const createAppMockRenderer = ({ features, - owner = [SECURITY_SOLUTION_OWNER], + owner = mockedTestProvidersOwner, permissions = allCasesPermissions(), releasePhase = 'ga', externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(), diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx index 6c33c86d29d51..7191767f780dd 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx @@ -25,6 +25,7 @@ const useKibanaMock = useKibana as jest.Mocked; describe('Use cases toast hook', () => { const successMock = jest.fn(); const errorMock = jest.fn(); + const dangerMock = jest.fn(); const getUrlForApp = jest.fn().mockReturnValue(`/app/cases/${mockCase.id}`); const navigateToUrl = jest.fn(); @@ -54,6 +55,7 @@ describe('Use cases toast hook', () => { return { addSuccess: successMock, addError: errorMock, + addDanger: dangerMock, }; }); @@ -352,4 +354,21 @@ describe('Use cases toast hook', () => { }); }); }); + + describe('showDangerToast', () => { + it('should show a danger toast', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + + result.current.showDangerToast('my danger toast'); + + expect(dangerMock).toHaveBeenCalledWith({ + title: 'my danger toast', + }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.tsx index 70ad598e863e9..a003226688011 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.tsx @@ -170,7 +170,7 @@ export const useCasesToast = () => { toasts.addSuccess({ title, className: 'eui-textBreakWord' }); }, showDangerToast: (title: string) => { - toasts.addDanger({ title, className: 'eui-textBreakWord' }); + toasts.addDanger({ title }); }, }; }; diff --git a/x-pack/plugins/cases/public/components/files/add_file.test.tsx b/x-pack/plugins/cases/public/components/files/add_file.test.tsx new file mode 100644 index 0000000000000..7be441c4cb7c5 --- /dev/null +++ b/x-pack/plugins/cases/public/components/files/add_file.test.tsx @@ -0,0 +1,217 @@ +/* + * 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 React from 'react'; + +import type { FileUploadProps } from '@kbn/shared-ux-file-upload'; + +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import type { AppMockRenderer } from '../../common/mock'; + +import { constructFileKindIdByOwner } from '../../../common/constants'; +import { + createAppMockRenderer, + mockedTestProvidersOwner, + mockedFilesClient, +} from '../../common/mock'; +import { AddFile } from './add_file'; +import { useToasts } from '../../common/lib/kibana'; + +import { useCreateAttachments } from '../../containers/use_create_attachments'; +import { basicFileMock } from '../../containers/mock'; + +jest.mock('../../containers/use_create_attachments'); +jest.mock('../../common/lib/kibana'); + +const useToastsMock = useToasts as jest.Mock; +const useCreateAttachmentsMock = useCreateAttachments as jest.Mock; + +const mockedExternalReferenceId = 'externalReferenceId'; +const validateMetadata = jest.fn(); +const mockFileUpload = jest + .fn() + .mockImplementation( + ({ + kind, + onDone, + onError, + meta, + }: Required>) => ( + <> + + + + + ) + ); + +jest.mock('@kbn/shared-ux-file-upload', () => { + const original = jest.requireActual('@kbn/shared-ux-file-upload'); + return { + ...original, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + FileUpload: (props: any) => mockFileUpload(props), + }; +}); + +describe('AddFile', () => { + let appMockRender: AppMockRenderer; + + const successMock = jest.fn(); + const errorMock = jest.fn(); + + useToastsMock.mockImplementation(() => { + return { + addSuccess: successMock, + addError: errorMock, + }; + }); + + const createAttachmentsMock = jest.fn(); + + useCreateAttachmentsMock.mockReturnValue({ + isLoading: false, + createAttachments: createAttachmentsMock, + }); + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders correctly', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('cases-files-add')).toBeInTheDocument(); + }); + + it('clicking button renders modal', async () => { + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + }); + + it('createAttachments called with right parameters', async () => { + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('testOnDone')); + + await waitFor(() => + expect(createAttachmentsMock).toBeCalledWith( + expect.objectContaining({ + caseId: 'foobar', + caseOwner: mockedTestProvidersOwner[0], + data: [ + { + externalReferenceAttachmentTypeId: '.files', + externalReferenceId: mockedExternalReferenceId, + externalReferenceMetadata: { + files: [ + { + createdAt: '2020-02-19T23:06:33.798Z', + extension: 'png', + mimeType: 'image/png', + name: 'my-super-cool-screenshot', + }, + ], + }, + externalReferenceStorage: { soType: 'file', type: 'savedObject' }, + type: 'externalReference', + }, + ], + throwOnError: true, + }) + ) + ); + await waitFor(() => + expect(successMock).toHaveBeenCalledWith({ + className: 'eui-textBreakWord', + title: `File ${basicFileMock.name} uploaded successfully`, + }) + ); + }); + + it('failed upload displays error toast', async () => { + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('testOnError')); + + expect(errorMock).toHaveBeenCalledWith( + { name: 'upload error name', message: 'upload error message' }, + { + title: 'Failed to upload file', + } + ); + }); + + it('correct metadata is passed to FileUpload component', async () => { + const caseId = 'foobar'; + + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('testMetadata')); + + await waitFor(() => + expect(validateMetadata).toHaveBeenCalledWith({ caseId, owner: mockedTestProvidersOwner[0] }) + ); + }); + + it('filesClient.delete is called correctly if createAttachments fails', async () => { + createAttachmentsMock.mockImplementation(() => { + throw new Error(); + }); + + appMockRender.render(); + + userEvent.click(await screen.findByTestId('cases-files-add')); + + expect(await screen.findByTestId('cases-files-add-modal')).toBeInTheDocument(); + + userEvent.click(await screen.findByTestId('testOnDone')); + + await waitFor(() => + expect(mockedFilesClient.delete).toHaveBeenCalledWith({ + id: mockedExternalReferenceId, + kind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + }) + ); + + createAttachmentsMock.mockRestore(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/files/add_file.tsx b/x-pack/plugins/cases/public/components/files/add_file.tsx index 1b4e36dca4297..e6081b3c6a64b 100644 --- a/x-pack/plugins/cases/public/components/files/add_file.tsx +++ b/x-pack/plugins/cases/public/components/files/add_file.tsx @@ -39,7 +39,6 @@ const AddFileComponent: React.FC = ({ caseId }) => { const { owner } = useCasesContext(); const { client: filesClient } = useFilesContext(); const { showDangerToast, showErrorToast, showSuccessToast } = useCasesToast(); - const { isLoading, createAttachments } = useCreateAttachments(); const refreshAttachmentsTable = useRefreshCaseViewPage(); const [isModalVisible, setIsModalVisible] = useState(false); diff --git a/x-pack/plugins/cases/public/components/files/files_table.test.tsx b/x-pack/plugins/cases/public/components/files/files_table.test.tsx index ed881ce985a64..ce78be54b3529 100644 --- a/x-pack/plugins/cases/public/components/files/files_table.test.tsx +++ b/x-pack/plugins/cases/public/components/files/files_table.test.tsx @@ -10,7 +10,13 @@ import { screen, within } from '@testing-library/react'; import { basicFileMock } from '../../containers/mock'; import type { AppMockRenderer } from '../../common/mock'; -import { createAppMockRenderer } from '../../common/mock'; + +import { constructFileKindIdByOwner } from '../../../common/constants'; +import { + createAppMockRenderer, + mockedFilesClient, + mockedTestProvidersOwner, +} from '../../common/mock'; import { FilesTable } from './files_table'; import userEvent from '@testing-library/user-event'; @@ -73,7 +79,7 @@ describe('FilesTable', () => { ) ); - expect(await screen.queryByTestId('case-files-image-preview')).not.toBeInTheDocument(); + expect(await screen.queryByTestId('cases-files-image-preview')).not.toBeInTheDocument(); }); it('image rows open file preview', async () => { @@ -84,6 +90,43 @@ describe('FilesTable', () => { name: `${basicFileMock.name}.${basicFileMock.extension}`, }) ); - expect(await screen.findByTestId('case-files-image-preview')).toBeInTheDocument(); + + expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); + }); + + it('different mimeTypes are displayed correctly', async () => { + const mockPagination = { pageIndex: 0, pageSize: 10, totalItemCount: 7 }; + appMockRender.render( + + ); + + expect((await screen.findAllByText('Unknown')).length).toBe(4); + expect(await screen.findByText('Application')).toBeInTheDocument(); + expect(await screen.findByText('Text')).toBeInTheDocument(); + expect(await screen.findByText('Image')).toBeInTheDocument(); + }); + + it('download button renders correctly', async () => { + appMockRender.render(); + + expect(mockedFilesClient.getDownloadHref).toBeCalledTimes(1); + expect(mockedFilesClient.getDownloadHref).toHaveBeenCalledWith({ + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + id: basicFileMock.id, + }); + + expect(await screen.findByTestId('cases-files-table-action-download')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/cases/public/components/files/translations.tsx b/x-pack/plugins/cases/public/components/files/translations.tsx index 55d3f679610d9..6ea6080d3cb33 100644 --- a/x-pack/plugins/cases/public/components/files/translations.tsx +++ b/x-pack/plugins/cases/public/components/files/translations.tsx @@ -55,16 +55,20 @@ export const TYPE = i18n.translate('xpack.cases.caseView.files.type', { defaultMessage: 'Type', }); -export const SEARCH_PLACEHOLDER = i18n.translate('xpack.cases.caseViewFiles.searchPlaceholder', { +export const SEARCH_PLACEHOLDER = i18n.translate('xpack.cases.caseView.files.searchPlaceholder', { defaultMessage: 'Search files', }); -export const FAILED_UPLOAD = i18n.translate('xpack.cases.caseView.failedUpload', { +export const FAILED_UPLOAD = i18n.translate('xpack.cases.caseView.files.failedUpload', { defaultMessage: 'Failed to upload file', }); +export const UNKNOWN_MIME_TYPE = i18n.translate('xpack.cases.caseView.files.unknownMimeType', { + defaultMessage: 'Unknown', +}); + export const SUCCESSFUL_UPLOAD_FILE_NAME = (fileName: string) => - i18n.translate('xpack.cases.caseView.successfulUploadFileName', { + i18n.translate('xpack.cases.caseView.files.successfulUploadFileName', { defaultMessage: 'File {fileName} uploaded successfully', values: { fileName }, }); diff --git a/x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx b/x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx index cceabca38e281..7477f71128162 100644 --- a/x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx +++ b/x-pack/plugins/cases/public/components/files/use_files_table_columns.tsx @@ -17,7 +17,7 @@ import type { Owner } from '../../../common/constants/types'; import { constructFileKindIdByOwner } from '../../../common/constants'; import { useCasesContext } from '../cases_context/use_cases_context'; import * as i18n from './translations'; -import { isImage } from './utils'; +import { isImage, parseMimeType } from './utils'; export interface FilesTableColumnsProps { showPreview: (file: FileJSON) => void; @@ -48,7 +48,7 @@ export const useFilesTableColumns = ({ name: i18n.TYPE, 'data-test-subj': 'cases-files-table-filetype', render: (attachment: FileJSON) => { - return {`${attachment.mimeType?.split('/')[0]}` || ''}; + return {parseMimeType(attachment.mimeType)}; }, }, { diff --git a/x-pack/plugins/cases/public/components/files/utils.tsx b/x-pack/plugins/cases/public/components/files/utils.tsx index 9a6a68cf6967e..dd18ee7d8e9cc 100644 --- a/x-pack/plugins/cases/public/components/files/utils.tsx +++ b/x-pack/plugins/cases/public/components/files/utils.tsx @@ -7,4 +7,20 @@ import type { FileJSON } from '@kbn/shared-ux-file-types'; +import * as i18n from './translations'; + export const isImage = (file: FileJSON) => file.mimeType?.startsWith('image/'); + +export const parseMimeType = (mimeType: string | undefined) => { + if (typeof mimeType === 'undefined') { + return i18n.UNKNOWN_MIME_TYPE; + } + + const result = mimeType.split('/'); + + if (result.length <= 1 || result[0] === '') { + return i18n.UNKNOWN_MIME_TYPE; + } + + return result[0].charAt(0).toUpperCase() + result[0].slice(1); +}; diff --git a/x-pack/plugins/cases/public/files/index.ts b/x-pack/plugins/cases/public/files/index.ts index 9066e9e53cabe..c3a051adf08d9 100644 --- a/x-pack/plugins/cases/public/files/index.ts +++ b/x-pack/plugins/cases/public/files/index.ts @@ -21,7 +21,7 @@ const buildFileKind = (owner: Owner): FileKindBrowser => { }; export const isRegisteredOwner = (ownerToCheck: string): ownerToCheck is Owner => - ownerToCheck in CASES_FILE_KINDS; + Object.hasOwn(CASES_FILE_KINDS, ownerToCheck); /** * The file kind definition for interacting with the file service for the UI