Skip to content

Commit

Permalink
[Security solution] Reinstall product documentation callout (elastic#…
Browse files Browse the repository at this point in the history
  • Loading branch information
stephmilovic authored Jan 10, 2025
1 parent 54436e3 commit ac45771
Show file tree
Hide file tree
Showing 20 changed files with 460 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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.
*/

export const REACT_QUERY_KEYS = {
GET_PRODUCT_DOC_STATUS: 'get_product_doc_status',
INSTALL_PRODUCT_DOC: 'install_product_doc',
};
Original file line number Diff line number Diff line change
@@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { waitFor, renderHook } from '@testing-library/react';
import { useGetProductDocStatus } from './use_get_product_doc_status';
import { useAssistantContext } from '../../../..';
import { TestProviders } from '../../../mock/test_providers/test_providers';

jest.mock('../../../..', () => ({
useAssistantContext: jest.fn(),
}));

describe('useGetProductDocStatus', () => {
const mockGetStatus = jest.fn();

beforeEach(() => {
(useAssistantContext as jest.Mock).mockReturnValue({
productDocBase: {
installation: {
getStatus: mockGetStatus,
},
},
});
});

it('returns loading state initially', async () => {
mockGetStatus.mockResolvedValueOnce('status');
const { result } = renderHook(() => useGetProductDocStatus(), {
wrapper: TestProviders,
});

expect(result.current.isLoading).toBe(true);
await waitFor(() => result.current.isSuccess);
});

it('returns success state with data', async () => {
mockGetStatus.mockResolvedValueOnce('status');
const { result } = renderHook(() => useGetProductDocStatus(), {
wrapper: TestProviders,
});

await waitFor(() => {
expect(result.current.status).toBe('status');
expect(result.current.isSuccess).toBe(true);
});
});

it('returns error state when query fails', async () => {
mockGetStatus.mockRejectedValueOnce(new Error('error'));
const { result } = renderHook(() => useGetProductDocStatus(), {
wrapper: TestProviders,
});

await waitFor(() => {
expect(result.current.isError).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 { useQuery } from '@tanstack/react-query';
import { REACT_QUERY_KEYS } from './const';
import { useAssistantContext } from '../../../..';

export function useGetProductDocStatus() {
const { productDocBase } = useAssistantContext();

const { isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery({
queryKey: [REACT_QUERY_KEYS.GET_PRODUCT_DOC_STATUS],
queryFn: async () => {
return productDocBase.installation.getStatus();
},
keepPreviousData: false,
refetchOnWindowFocus: false,
});

return {
status: data,
refetch,
isLoading,
isRefetching,
isSuccess,
isError,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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 { waitFor, renderHook } from '@testing-library/react';
import { useInstallProductDoc } from './use_install_product_doc';
import { useAssistantContext } from '../../../..';
import { TestProviders } from '../../../mock/test_providers/test_providers';

jest.mock('../../../..', () => ({
useAssistantContext: jest.fn(),
}));

describe('useInstallProductDoc', () => {
const mockInstall = jest.fn();
const mockAddSuccess = jest.fn();
const mockAddError = jest.fn();

beforeEach(() => {
(useAssistantContext as jest.Mock).mockReturnValue({
productDocBase: {
installation: {
install: mockInstall,
},
},
toasts: {
addSuccess: mockAddSuccess,
addError: mockAddError,
},
});
});

it('returns success state and shows success toast on successful installation', async () => {
mockInstall.mockResolvedValueOnce({});
const { result } = renderHook(() => useInstallProductDoc(), {
wrapper: TestProviders,
});

result.current.mutate();
await waitFor(() => result.current.isSuccess);

expect(mockAddSuccess).toHaveBeenCalledWith(
'The Elastic documentation was successfully installed'
);
});

it('returns error state and shows error toast on failed installation', async () => {
const error = new Error('error message');
mockInstall.mockRejectedValueOnce(error);
const { result } = renderHook(() => useInstallProductDoc(), {
wrapper: TestProviders,
});

result.current.mutate();
await waitFor(() => result.current.isError);

expect(mockAddError).toHaveBeenCalledWith(
expect.objectContaining({
message: 'error message',
}),
{ title: 'Something went wrong while installing the Elastic documentation' }
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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 { useMutation, useQueryClient } from '@tanstack/react-query';
import { i18n } from '@kbn/i18n';
import type { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import type { PerformInstallResponse } from '@kbn/product-doc-base-plugin/common/http_api/installation';
import { REACT_QUERY_KEYS } from './const';
import { useAssistantContext } from '../../../..';

type ServerError = IHttpFetchError<ResponseErrorBody>;

export function useInstallProductDoc() {
const { productDocBase, toasts } = useAssistantContext();
const queryClient = useQueryClient();

return useMutation<PerformInstallResponse, ServerError, void>(
[REACT_QUERY_KEYS.INSTALL_PRODUCT_DOC],
() => {
return productDocBase.installation.install();
},
{
onSuccess: () => {
toasts?.addSuccess(
i18n.translate('xpack.elasticAssistant.kb.installProductDoc.successNotification', {
defaultMessage: 'The Elastic documentation was successfully installed',
})
);

queryClient.invalidateQueries({
queryKey: [REACT_QUERY_KEYS.GET_PRODUCT_DOC_STATUS],
refetchType: 'all',
});
},
onError: (error) => {
toasts?.addError(new Error(error.body?.message ?? error.message), {
title: i18n.translate('xpack.elasticAssistant.kb.installProductDoc.errorNotification', {
defaultMessage: 'Something went wrong while installing the Elastic documentation',
}),
});
},
}
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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 { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { ProductDocumentationManagement } from '.';
import * as i18n from './translations';
import { useInstallProductDoc } from '../../api/product_docs/use_install_product_doc';
import { useGetProductDocStatus } from '../../api/product_docs/use_get_product_doc_status';

jest.mock('../../api/product_docs/use_install_product_doc');
jest.mock('../../api/product_docs/use_get_product_doc_status');

describe('ProductDocumentationManagement', () => {
const mockInstallProductDoc = jest.fn().mockResolvedValue({});

beforeEach(() => {
(useInstallProductDoc as jest.Mock).mockReturnValue({ mutateAsync: mockInstallProductDoc });
(useGetProductDocStatus as jest.Mock).mockReturnValue({ status: null, isLoading: false });
jest.clearAllMocks();
});

it('renders loading spinner when status is loading', async () => {
(useGetProductDocStatus as jest.Mock).mockReturnValue({
status: { overall: 'not_installed' },
isLoading: true,
});
render(<ProductDocumentationManagement />);
expect(screen.getByTestId('statusLoading')).toBeInTheDocument();
});

it('renders install button when not installed', () => {
(useGetProductDocStatus as jest.Mock).mockReturnValue({
status: { overall: 'not_installed' },
isLoading: false,
});
render(<ProductDocumentationManagement />);
expect(screen.getByText(i18n.INSTALL)).toBeInTheDocument();
});

it('does not render anything when already installed', () => {
(useGetProductDocStatus as jest.Mock).mockReturnValue({
status: { overall: 'installed' },
isLoading: false,
});
const { container } = render(<ProductDocumentationManagement />);
expect(container).toBeEmptyDOMElement();
});

it('shows installing spinner and text when installing', async () => {
(useGetProductDocStatus as jest.Mock).mockReturnValue({
status: { overall: 'not_installed' },
isLoading: false,
});
render(<ProductDocumentationManagement />);
fireEvent.click(screen.getByText(i18n.INSTALL));
await waitFor(() => {
expect(screen.getByTestId('installing')).toBeInTheDocument();
expect(screen.getByText(i18n.INSTALLING)).toBeInTheDocument();
});
});

it('sets installed state to true after successful installation', async () => {
(useGetProductDocStatus as jest.Mock).mockReturnValue({
status: { overall: 'not_installed' },
isLoading: false,
});
mockInstallProductDoc.mockResolvedValueOnce({});
render(<ProductDocumentationManagement />);
fireEvent.click(screen.getByText(i18n.INSTALL));
await waitFor(() => expect(screen.queryByText(i18n.INSTALL)).not.toBeInTheDocument());
});

it('sets installed state to false after failed installation', async () => {
(useGetProductDocStatus as jest.Mock).mockReturnValue({
status: { overall: 'not_installed' },
isLoading: false,
});
mockInstallProductDoc.mockRejectedValueOnce(new Error('Installation failed'));
render(<ProductDocumentationManagement />);
fireEvent.click(screen.getByText(i18n.INSTALL));
await waitFor(() => expect(screen.getByText(i18n.INSTALL)).toBeInTheDocument());
});
});
Loading

0 comments on commit ac45771

Please sign in to comment.