Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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 } from '@testing-library/react';
import { DeprecatedRulesCallout } from './deprecated_rules_callout';

describe('DeprecatedRulesCallout', () => {
const defaultProps = {
title: 'Test Callout Title',
description: 'Test callout description',
buttons: [
<button type="button" key="action">
{'Do Action'}
</button>,
],
};

it('renders the title', () => {
render(<DeprecatedRulesCallout {...defaultProps} />);
expect(screen.getByText('Test Callout Title')).toBeInTheDocument();
});

it('renders the description', () => {
render(<DeprecatedRulesCallout {...defaultProps} />);
expect(screen.getByTestId('deprecated-rule-callout-description')).toHaveTextContent(
'Test callout description'
);
});

it('renders the provided buttons', () => {
render(<DeprecatedRulesCallout {...defaultProps} />);
expect(screen.getByText('Do Action')).toBeInTheDocument();
});

it('displays deprecation reason when provided', () => {
render(<DeprecatedRulesCallout {...defaultProps} reason="Replaced by rule XYZ" />);

const reasonEl = screen.getByTestId('deprecated-rule-reason');
expect(reasonEl).toBeInTheDocument();
expect(reasonEl).toHaveTextContent('Replaced by rule XYZ');
});

it('does not display deprecation reason section when reason is absent', () => {
render(<DeprecatedRulesCallout {...defaultProps} />);

expect(screen.queryByTestId('deprecated-rule-reason')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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 { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useUserPrivileges } from '../../../../common/components/user_privileges';
import { useExecuteBulkAction } from '../../logic/bulk_actions/use_execute_bulk_action';
import { DeprecatedRulesModal } from './deprecated_rules_modal';
import type { DeprecatedRuleForReview } from '../../../../../common/api/detection_engine/prebuilt_rules';

jest.mock('../../../../common/components/user_privileges');
jest.mock('../../logic/bulk_actions/use_execute_bulk_action');

const mockUseUserPrivileges = useUserPrivileges as jest.Mock;
const mockUseExecuteBulkAction = useExecuteBulkAction as jest.Mock;

// Simplified RuleLink component for testing.
jest.mock('../../../rule_management_ui/components/rules_table/use_columns', () => ({
RuleLink: ({ name, id }: { name: string; id: string }) => (
<a href={`/rules/id/${id}`} data-test-subj="ruleName">
{name}
</a>
),
}));

const mockExecuteBulkAction = jest.fn();
const mockOnClose = jest.fn();

const MOCK_RULES: DeprecatedRuleForReview[] = [
{ id: 'rule-so-id-1', rule_id: 'rule-rule-id-1', name: 'Deprecated Rule A' },
{ id: 'rule-so-id-2', rule_id: 'rule-rule-id-2', name: 'Deprecated Rule B' },
];

describe('DeprecatedRulesModal', () => {
beforeEach(() => {
jest.clearAllMocks();

mockUseUserPrivileges.mockReturnValue({
rulesPrivileges: {
rules: { edit: true },
exceptions: { edit: true },
},
});

mockUseExecuteBulkAction.mockReturnValue({ executeBulkAction: mockExecuteBulkAction });
});

describe('Delete all button is disabled for read-only users', () => {
it('disables the delete-all button when user cannot edit rules', () => {
mockUseUserPrivileges.mockReturnValue({
rulesPrivileges: {
rules: { edit: false },
exceptions: { edit: false },
},
});

render(<DeprecatedRulesModal rules={MOCK_RULES} isLoading={false} onClose={mockOnClose} />);

expect(screen.getByTestId('deprecated-rules-modal-delete-all')).toBeDisabled();
});

it('enables the delete-all button when user can edit rules', () => {
render(<DeprecatedRulesModal rules={MOCK_RULES} isLoading={false} onClose={mockOnClose} />);

expect(screen.getByTestId('deprecated-rules-modal-delete-all')).not.toBeDisabled();
});
});

describe('Cancel bulk delete confirmation modal', () => {
it('does not delete rules when the confirmation is cancelled', async () => {
render(<DeprecatedRulesModal rules={MOCK_RULES} isLoading={false} onClose={mockOnClose} />);

// Open the confirm modal
await act(async () => {
await userEvent.click(screen.getByTestId('deprecated-rules-modal-delete-all'));
});

expect(screen.getByTestId('deprecated-rules-delete-confirm-modal')).toBeInTheDocument();

// Click the cancel button inside the confirmation modal
await act(async () => {
await userEvent.click(screen.getByTestId('confirmModalCancelButton'));
});

// The confirm modal should be dismissed and no deletion should have been triggered
expect(screen.queryByTestId('deprecated-rules-delete-confirm-modal')).not.toBeInTheDocument();
expect(mockExecuteBulkAction).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
* 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 { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { RuleResponse } from '../../../../../common/api/detection_engine';
import { BulkActionTypeEnum } from '../../../../../common/api/detection_engine/rule_management';
import { DuplicateOptions } from '../../../../../common/detection_engine/rule_management/constants';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useUserPrivileges } from '../../../../common/components/user_privileges';
import { useExecuteBulkAction } from '../../logic/bulk_actions/use_execute_bulk_action';
import { usePrebuiltRulesDeprecationReview } from '../../logic/prebuilt_rules/use_prebuilt_rules_deprecation_review';
import { useKibana } from '../../../../common/lib/kibana';
import { savedRuleMock } from '../../logic/mock';
import { useDeprecatedRuleDetailsCallout } from './use_deprecated_rule_details_callout';
import { createDefaultExternalRuleSource } from '../../../../../server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/create_default_external_rule_source';

jest.mock('../../../../common/hooks/use_experimental_features');
jest.mock('../../../../common/components/user_privileges');
jest.mock('../../logic/bulk_actions/use_execute_bulk_action');
jest.mock('../../logic/prebuilt_rules/use_prebuilt_rules_deprecation_review');
jest.mock('../../../../common/lib/kibana');

const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
const mockUseUserPrivileges = useUserPrivileges as jest.Mock;
const mockUseExecuteBulkAction = useExecuteBulkAction as jest.Mock;
const mockUsePrebuiltRulesDeprecationReview = usePrebuiltRulesDeprecationReview as jest.Mock;
const mockUseKibana = useKibana as jest.Mock;

const RULE_ID = savedRuleMock.id;
const RULE_RULE_ID = savedRuleMock.rule_id;

// A prebuilt rule (external rule_source) for tests
const mockPrebuiltRule: RuleResponse = {
...savedRuleMock,
rule_source: createDefaultExternalRuleSource(),
};

const mockExecuteBulkAction = jest.fn();
const mockNavigateToApp = jest.fn();
const mockConfirmDeletion = jest.fn();
const mockShowBulkDuplicateExceptionsConfirmation = jest.fn();

/**
* Renders a wrapper component that calls the hook and renders the returned callout.
*/
function TestComponent({
rule = mockPrebuiltRule,
confirmDeletion = mockConfirmDeletion,
showBulkDuplicateExceptionsConfirmation = mockShowBulkDuplicateExceptionsConfirmation,
}: Partial<{
rule: RuleResponse | null;
confirmDeletion: () => Promise<boolean>;
showBulkDuplicateExceptionsConfirmation: () => Promise<string | null>;
}>) {
const callout = useDeprecatedRuleDetailsCallout({
rule,
confirmDeletion,
showBulkDuplicateExceptionsConfirmation,
});

return <>{callout}</>;
}

describe('useDeprecatedRuleDetailsCallout', () => {
beforeEach(() => {
jest.clearAllMocks();

mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);

mockUseUserPrivileges.mockReturnValue({
rulesPrivileges: {
rules: { edit: true },
exceptions: { edit: true },
},
});

mockUseExecuteBulkAction.mockReturnValue({ executeBulkAction: mockExecuteBulkAction });

// We utilize the enabled flag in the react query call, this mimics that behavior
mockUsePrebuiltRulesDeprecationReview.mockImplementation((_request, options) => {
if (options?.enabled === false) {
return { data: undefined, isLoading: false };
}
return {
data: {
rules: [{ id: RULE_ID, rule_id: RULE_RULE_ID, name: savedRuleMock.name }],
},
isLoading: false,
};
});

mockUseKibana.mockReturnValue({
services: {
application: { navigateToApp: mockNavigateToApp },
},
});
});

describe('Original rule is not deleted if duplication fails', () => {
it('does not call delete when duplicate bulk action returns no created rules', async () => {
mockExecuteBulkAction.mockResolvedValue({
attributes: { results: { created: [] } },
});
mockShowBulkDuplicateExceptionsConfirmation.mockResolvedValue(
DuplicateOptions.withoutExceptions
);

render(<TestComponent />);

const duplicateButton = screen.getByTestId('deprecated-rule-duplicate-and-delete-button');
await act(async () => {
await userEvent.click(duplicateButton);
});

expect(mockExecuteBulkAction).toHaveBeenCalledTimes(1);
expect(mockExecuteBulkAction).toHaveBeenCalledWith(
expect.objectContaining({ type: BulkActionTypeEnum.duplicate })
);
expect(mockExecuteBulkAction).not.toHaveBeenCalledWith(
expect.objectContaining({ type: BulkActionTypeEnum.delete })
);
});

it('does not call delete when duplicate bulk action returns undefined', async () => {
mockExecuteBulkAction.mockResolvedValue(undefined);
mockShowBulkDuplicateExceptionsConfirmation.mockResolvedValue(
DuplicateOptions.withoutExceptions
);

render(<TestComponent />);

const duplicateButton = screen.getByTestId('deprecated-rule-duplicate-and-delete-button');
await act(async () => {
await userEvent.click(duplicateButton);
});

expect(mockExecuteBulkAction).toHaveBeenCalledTimes(1);
expect(mockExecuteBulkAction).not.toHaveBeenCalledWith(
expect.objectContaining({ type: BulkActionTypeEnum.delete })
);
});
});

describe('Action buttons are disabled for read-only users', () => {
beforeEach(() => {
mockUseUserPrivileges.mockReturnValue({
rulesPrivileges: {
rules: { edit: false },
exceptions: { edit: false },
},
});
});

it('disables both delete and duplicate-and-delete buttons when user cannot edit rules', () => {
render(<TestComponent />);

expect(screen.getByTestId('deprecated-rule-delete-button')).toBeDisabled();
expect(screen.getByTestId('deprecated-rule-duplicate-and-delete-button')).toBeDisabled();
});
});

describe('Cancel duplicate-and-delete flow', () => {
it('does not duplicate or delete when exceptions confirmation is cancelled', async () => {
mockShowBulkDuplicateExceptionsConfirmation.mockResolvedValue(null);

render(<TestComponent />);

const duplicateButton = screen.getByTestId('deprecated-rule-duplicate-and-delete-button');
await act(async () => {
await userEvent.click(duplicateButton);
});

expect(mockExecuteBulkAction).not.toHaveBeenCalled();
});
});

describe('Cancel delete flow', () => {
it('does not delete when deletion confirmation returns false', async () => {
mockConfirmDeletion.mockResolvedValue(false);

render(<TestComponent />);

const deleteButton = screen.getByTestId('deprecated-rule-delete-button');
await act(async () => {
await userEvent.click(deleteButton);
});

expect(mockExecuteBulkAction).not.toHaveBeenCalled();
});
});

describe('Returns null when callout should not show', () => {
it('returns null when rule is null', () => {
const { container } = render(<TestComponent rule={null} />);

expect(container).toBeEmptyDOMElement();
});

it('returns null when rule is a custom rule (not external)', () => {
const customRule: RuleResponse = { ...savedRuleMock };

const { container } = render(<TestComponent rule={customRule} />);

expect(container).toBeEmptyDOMElement();
});

it('returns null while deprecation data is loading', () => {
mockUsePrebuiltRulesDeprecationReview.mockReturnValue({ data: undefined, isLoading: true });

const { container } = render(<TestComponent />);

expect(container).toBeEmptyDOMElement();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { DeprecatedRulesCallout } from './deprecated_rules_callout';
import { DeprecatedRulesModal } from './deprecated_rules_modal';
import * as i18n from './translations';

const DISMISSAL_STORAGE_KEY = 'securitySolution.deprecatedRulesCallout.dismissedAt';
export const DISMISSAL_STORAGE_KEY = 'securitySolution.deprecatedRulesCallout.dismissedAt';

export const useDeprecatedRulesTableCallout = () => {
const isFeatureEnabled = useIsExperimentalFeatureEnabled('prebuiltRulesDeprecationUIEnabled');
Expand Down
Loading
Loading