Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -45,7 +45,7 @@ https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one
- [Deprecation review: with ids filter](#deprecation-review-with-ids-filter)
- [**Scenario: Review filters by installed rule SO ids**](#scenario-review-filters-by-installed-rule-so-ids)
- [**Scenario: Review returns empty when filtered rule is not deprecated**](#scenario-review-returns-empty-when-filtered-rule-is-not-deprecated)
- [**Scenario: Review returns empty when filtered id does not exist**](#scenario-review-returns-empty-when-filtered-id-does-not-exist)
- [**Scenario: Review returns 400 when filtered id does not exist**](#scenario-review-returns-400-when-filtered-id-does-not-exist)
- [Deprecation review: edge cases](#deprecation-review-edge-cases)
- [**Scenario: Review respects MAX\_DEPRECATED\_RULES\_TO\_RETURN limit**](#scenario-review-respects-max_deprecated_rules_to_return-limit)
- [**Scenario: Review handles package with no deprecated rules**](#scenario-review-handles-package-with-no-deprecated-rules)
Expand Down Expand Up @@ -239,14 +239,14 @@ When the user requests the deprecation review filtered to rule D
Then the response contains an empty rules array
```

#### **Scenario: Review returns empty when filtered id does not exist**
#### **Scenario: Review returns 400 when filtered id does not exist**

**Automation**: API integration tests.

```Gherkin
Given a non-existent rule SO id
When the user requests the deprecation review filtered to the non-existent id
Then the response contains an empty rules array
Then the endpoint returns a 400 error with message "No rules found for bulk get"
```

### Deprecation review: edge cases
Expand Down
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();
});
});
});
Loading
Loading