[Security Solution] Add rule upgrade preview FE integration tests (Rule Upgrade Flyout) (#210377)

**Partially addresses:** https://github.com/elastic/kibana/pull/205645

## Summary

This PR implements Frontend integration tests from the [upgrading prebuilt rules one-by-one with preview test plan](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/prebuilt_rule_upgrade_with_preview.md).

## Details

This PR add Jest integration tests (`@kbn/test/jest_integration` preset) for Rule Upgrade Flyout. Test scenarios are described in [upgrading prebuilt rules one-by-one with preview test plan](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/prebuilt_rule_upgrade_with_preview.md).

Tests cover each `diffable rule` field separately to guarantee visibility on broken functionality.

`esql_query` and `threat_mapping` fields were skipped due to mocking difficulties.

### Tests setup

- Rules Management page is used as the root component to test functionality in integration
- HTTP responses are mocked via mocking return values from `Kibana.http.fetch()` method
- Test scenarios are the same for each diffable rule field and moved out to reusable utility functions
This commit is contained in:
Maxim Palenov 2025-05-01 11:39:17 +02:00 committed by GitHub
parent 4416bc8bf5
commit ea7cccab08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 4846 additions and 28 deletions

View file

@ -284,6 +284,7 @@ module.exports = {
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]detection_engine[\/\\]rule_management_ui[\/\\]components[\/\\]rules_table[\/\\]rules_table_filters[\/\\]rules_table_filters.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]detection_engine[\/\\]rule_management_ui[\/\\]components[\/\\]rules_table[\/\\]upgrade_prebuilt_rules_table[\/\\]upgrade_prebuilt_rules_table_filters.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]detection_engine[\/\\]rule_management_ui[\/\\]components[\/\\]rules_table[\/\\]upgrade_prebuilt_rules_table[\/\\]use_ml_jobs_upgrade_modal[\/\\]ml_jobs_upgrade_modal.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]detection_engine[\/\\]rule_management_ui[\/\\]pages[\/\\]rule_management[\/\\]__integration_tests__[\/\\]rules_upgrade[\/\\]test_utils[\/\\]rule_upgrade_test_providers.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]detection_engine[\/\\]rule_response_actions[\/\\]response_action_type_form.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]detection_engine[\/\\]rule_response_actions[\/\\]response_actions_form.test.tsx/,
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]detections[\/\\]components[\/\\]alerts_kpis[\/\\]alerts_by_rule_panel[\/\\]alerts_by_rule.tsx/,

View file

@ -35,12 +35,22 @@ const createStartContract = (): Start => {
},
getDefaultDataView: jest.fn().mockReturnValue(Promise.resolve({})),
getDefaultId: jest.fn().mockReturnValue(Promise.resolve('')),
get: jest.fn().mockReturnValue(Promise.resolve({})),
get: jest.fn().mockReturnValue(
Promise.resolve({
title: '',
fields: [],
})
),
clearCache: jest.fn(),
getCanSaveSync: jest.fn(),
getIdsWithTitle: jest.fn(),
getIdsWithTitle: jest.fn().mockResolvedValue([]),
getFieldsForIndexPattern: jest.fn(),
create: jest.fn().mockReturnValue(Promise.resolve({})),
create: jest.fn().mockReturnValue(
Promise.resolve({
title: '',
fields: [],
})
),
toDataView: jest.fn().mockReturnValue(Promise.resolve({})),
toDataViewLazy: jest.fn().mockReturnValue(Promise.resolve({})),
clearInstanceCache: jest.fn(),

View file

@ -118,7 +118,7 @@ const extractDiffableCommonFields = (
version: rule.version,
// Main domain fields
name: rule.name.trim(),
name: rule.name?.trim(),
tags: rule.tags ?? [],
description: rule.description,
severity: rule.severity,

View file

@ -64,7 +64,7 @@ interface ExtractRuleEqlQueryParams {
export const extractRuleEqlQuery = (params: ExtractRuleEqlQueryParams): RuleEqlQuery => {
return {
query: params.query.trim(),
query: params.query?.trim(),
language: params.language,
filters: normalizeFilterArray(params.filters),
event_category_override: params.eventCategoryOverride,
@ -78,7 +78,7 @@ export const extractRuleEsqlQuery = (
language: EsqlQueryLanguage
): RuleEsqlQuery => {
return {
query: query.trim(),
query: query?.trim(),
language,
};
};

View file

@ -13,7 +13,7 @@ import type {
} from '../../../api/detection_engine/model/rule_schema';
export const extractThreatArray = (rule: RuleResponse): ThreatArray =>
rule.threat.map((threat) => {
rule.threat?.map((threat) => {
if (threat.technique && threat.technique.length) {
return {
...threat,
@ -26,7 +26,7 @@ export const extractThreatArray = (rule: RuleResponse): ThreatArray =>
tactic: { ...threat.tactic, reference: normalizeThreatReference(threat.tactic.reference) },
technique: undefined,
}; // If `technique` is an empty array, remove the field from the `threat` object
});
}) ?? [];
const trimTechniqueArray = (techniqueArray: ThreatTechnique[]): ThreatTechnique[] => {
return techniqueArray.map((technique) => ({

View file

@ -14,6 +14,7 @@ import { coreMock, themeServiceMock } from '@kbn/core/public/mocks';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { securityMock } from '@kbn/security-plugin/public/mocks';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import {
DEFAULT_APP_REFRESH_INTERVAL,
@ -59,6 +60,7 @@ import { calculateBounds } from '@kbn/data-plugin/common';
import { alertingPluginMock } from '@kbn/alerting-plugin/public/mocks';
import { createTelemetryServiceMock } from '../telemetry/telemetry_service.mock';
import { createSiemMigrationsMock } from '../../mock/mock_siem_migrations_service';
import { KibanaServices } from './services';
const mockUiSettings: Record<string, unknown> = {
[DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' },
@ -215,6 +217,13 @@ export const createStartServicesMock = (
showQueries: true,
saveQuery: true,
},
maintenanceWindow: {
show: true,
save: true,
},
actions: {
show: true,
},
},
},
security,
@ -261,6 +270,12 @@ export const createStartServicesMock = (
timelineDataService,
alerting,
siemMigrations,
sessionStorage: new Storage({
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
}),
} as unknown as StartServices;
};
@ -276,6 +291,14 @@ export const createWithKibanaMock = () => {
export const createKibanaContextProviderMock = () => {
const services = createStartServicesMock();
KibanaServices.init({
...services,
kibanaBranch: 'test',
kibanaVersion: 'test',
buildFlavor: 'test',
prebuiltRulesPackageVersion: 'test',
});
// eslint-disable-next-line react/display-name
return ({
children,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { act, fireEvent, waitFor } from '@testing-library/react';
import { act, fireEvent, waitFor, within } from '@testing-library/react';
export function showEuiComboBoxOptions(comboBoxToggleButton: HTMLElement): Promise<void> {
fireEvent.click(comboBoxToggleButton);
@ -60,6 +60,28 @@ export function selectEuiComboBoxOption({
});
}
interface AddEuiComboBoxOptionParameters {
wrapper: HTMLElement;
optionText: string;
}
export async function addEuiComboBoxOption({
wrapper,
optionText,
}: AddEuiComboBoxOptionParameters): Promise<void> {
const input = within(wrapper).getByRole('combobox');
await act(async () => {
fireEvent.change(input, {
target: { value: optionText },
});
});
await act(async () => {
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 });
});
}
export function selectFirstEuiComboBoxOption({
comboBoxToggleButton,
}: {
@ -68,12 +90,21 @@ export function selectFirstEuiComboBoxOption({
return selectEuiComboBoxOption({ comboBoxToggleButton, optionIndex: 0 });
}
export function clearEuiComboBoxSelection({
export async function clearEuiComboBoxSelection({
clearButton,
}: {
clearButton: HTMLElement;
}): Promise<void> {
return act(async () => {
const toggleButton = clearButton.nextElementSibling;
await act(async () => {
fireEvent.click(clearButton);
});
if (toggleButton) {
// Make sure options list gets closed
await act(async () => {
fireEvent.click(toggleButton);
});
}
}

View file

@ -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 { act, fireEvent, waitFor } from '@testing-library/react';
export function showEuiSuperSelectOptions(toggleButton: HTMLElement): Promise<void> {
fireEvent.click(toggleButton);
return waitFor(() => {
const listWithOptionsElement = document.querySelector('[role="listbox"]');
const emptyListElement = document.querySelector('.euiComboBoxOptionsList__empty');
expect(listWithOptionsElement || emptyListElement).toBeInTheDocument();
});
}
type SelectEuiSuperSelectOptionParameters =
| {
toggleButton: HTMLElement;
optionIndex: number;
optionText?: undefined;
}
| {
toggleButton: HTMLElement;
optionText: string;
optionIndex?: undefined;
};
export function selectEuiSuperSelectOption({
toggleButton,
optionIndex,
optionText,
}: SelectEuiSuperSelectOptionParameters): Promise<void> {
return act(async () => {
await showEuiSuperSelectOptions(toggleButton);
const options = Array.from(document.querySelectorAll('[role="listbox"] [role="option"]'));
if (typeof optionText === 'string') {
const lowerCaseOptionText = optionText.toLocaleLowerCase();
const optionToSelect = options.find(
(option) => option.textContent?.toLowerCase() === lowerCaseOptionText
);
if (optionToSelect) {
fireEvent.click(optionToSelect);
} else {
throw new Error(
`Could not find option with text "${optionText}". Available options: ${options
.map((option) => option.textContent)
.join(', ')}`
);
}
} else {
fireEvent.click(options[optionIndex]);
}
});
}

View file

@ -9,6 +9,7 @@ module.exports = {
preset: '@kbn/test',
rootDir: '../../../../../../..',
roots: ['<rootDir>/x-pack/solutions/security/plugins/security_solution/public/detection_engine'],
modulePathIgnorePatterns: ['__integration_tests__'],
coverageDirectory:
'<rootDir>/target/kibana-coverage/jest/x-pack/solutions/security/plugins/security_solution/public/detection_engine',
coverageReporters: ['text', 'html'],

View file

@ -85,11 +85,14 @@ export function RelatedIntegrationField({
);
const handleVersionChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) =>
(e: ChangeEvent<HTMLInputElement>) => {
const version = e.target.value;
field.setValue((oldValue) => ({
...oldValue,
version: e.target.value,
})),
version,
}));
},
[field]
);

View file

@ -179,6 +179,7 @@ const RequiredFieldsList = ({
}
hasChildLabel={false}
labelType="legend"
data-test-subj="requiredFieldsFormRow"
>
<>
{items.map((item) => (

View file

@ -47,6 +47,7 @@ export const AnomalyThresholdSlider = ({
tickInterval={25}
min={0}
max={100}
data-test-subj="anomalyThresholdRange"
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -7,7 +7,7 @@
import React, { useState, useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { isEqual } from 'lodash';
import { isEqual, snakeCase } from 'lodash';
import usePrevious from 'react-use/lib/usePrevious';
import { KibanaSectionErrorBoundary } from '@kbn/shared-ux-error-boundary';
import { VersionsPicker, VersionsPickerOptionEnum } from './versions_picker/versions_picker';
@ -58,7 +58,7 @@ export function FieldComparisonSide(): JSX.Element {
}, [selectedOption, prevResolvedValue, resolvedValue]);
return (
<>
<section data-test-subj={`${snakeCase(fieldName)}-comparisonSide`}>
<FieldUpgradeSideHeader>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
@ -81,6 +81,6 @@ export function FieldComparisonSide(): JSX.Element {
<KibanaSectionErrorBoundary sectionName={i18n.TITLE}>
<SubfieldChanges fieldName={fieldName} subfieldChanges={subfieldChanges} />
</KibanaSectionErrorBoundary>
</>
</section>
);
}

View file

@ -6,15 +6,21 @@
*/
import React from 'react';
import { snakeCase } from 'lodash';
import { useFieldUpgradeContext } from '../../rule_upgrade/field_upgrade_context';
import { FieldEditFormContextProvider } from '../context/field_edit_form_context';
import { FieldFinalSideContent } from './field_final_side_content';
import { FieldFinalSideHeader } from './field_final_side_header';
export function FieldFinalSide(): JSX.Element {
const { fieldName } = useFieldUpgradeContext();
return (
<FieldEditFormContextProvider>
<FieldFinalSideHeader />
<FieldFinalSideContent />
</FieldEditFormContextProvider>
<section data-test-subj={`${snakeCase(fieldName)}-finalSide`}>
<FieldEditFormContextProvider>
<FieldFinalSideHeader />
<FieldFinalSideContent />
</FieldEditFormContextProvider>
</section>
);
}

View file

@ -21,7 +21,7 @@ export function BuildingBlockEdit(): JSX.Element {
export function buildingBlockDeserializer(defaultValue: FormData) {
return {
isBuildingBlock: defaultValue.building_block,
isBuildingBlock: Boolean(defaultValue.building_block),
};
}

View file

@ -29,6 +29,10 @@ export function useDataView(indexPatternsOrDataViewId: UseDataViewParams): UseDa
setIsLoading(true);
(async () => {
if (dataView !== undefined) {
return;
}
try {
if (indexPatternsOrDataViewId.indexPatterns) {
const indexPatternsDataView = await dataViewsService.create({
@ -51,6 +55,7 @@ export function useDataView(indexPatternsOrDataViewId: UseDataViewParams): UseDa
}
})();
}, [
dataView,
dataViewsService,
indexPatternsOrDataViewId.indexPatterns,
indexPatternsOrDataViewId.dataViewId,

View file

@ -32,10 +32,12 @@ export function SimpleRuleScheduleAdapter(): JSX.Element {
const INTERVAL_COMPONENT_PROPS = {
minValue: 1,
dataTestSubj: 'intervalFormRow',
};
const LOOKBACK_COMPONENT_PROPS = {
minValue: 0,
dataTestSubj: 'lookbackFormRow',
};
const INTERVAL_FIELD_CONFIG: FieldConfig<string> = {

View file

@ -6,6 +6,7 @@
*/
import React from 'react';
import { snakeCase } from 'lodash';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
import { SplitAccordion } from '../../../../../../common/components/split_accordion';
@ -29,7 +30,7 @@ export function FieldUpgrade(): JSX.Element {
/>
}
initialIsOpen={hasConflict}
data-test-subj="ruleUpgradePerFieldDiff"
data-test-subj={`${snakeCase(fieldName)}-upgrade`}
>
<EuiFlexGroup gutterSize="s" alignItems="flexStart">
<EuiFlexItem grow={1}>

View file

@ -0,0 +1,574 @@
/*
* 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 { act, fireEvent, within } from '@testing-library/react';
import {
ThreeWayDiffConflict,
ThreeWayDiffOutcome,
} from '../../../../../../../common/api/detection_engine';
import { VersionsPickerOptionEnum } from '../../../../../rule_management/components/rule_details/three_way_diff/comparison_side/versions_picker/versions_picker';
import {
mockRuleUpgradeReviewData,
renderRuleUpgradeFlyout,
} from './test_utils/rule_upgrade_flyout';
import {
acceptSuggestedFieldValue,
saveAndAcceptFieldValue,
saveFieldValue,
switchToFieldEdit,
toggleFieldAccordion,
} from './test_utils/rule_upgrade_helpers';
import { inputFieldValue } from './test_utils/set_field_value';
describe('Rule upgrade preview Diff View options', () => {
describe('non-customized field w/ an upgrade (AAB)', () => {
it('shows default (incoming upgrade)', async () => {
const { diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Initial name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
});
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('Changes from Elastic');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Updated name');
});
it('shows resolved value', async () => {
const { fieldUpgradeWrapper, diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Initial name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
});
switchToFieldEdit(fieldUpgradeWrapper);
await inputFieldValue(fieldUpgradeWrapper, { fieldName: 'name', value: 'Resolved name' });
await saveFieldValue(fieldUpgradeWrapper);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('My changes');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Resolved name');
});
it('shows the same diff after saving unchanged field value', async () => {
const { fieldUpgradeWrapper, diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Initial name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
});
switchToFieldEdit(fieldUpgradeWrapper);
await saveFieldValue(fieldUpgradeWrapper);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('Changes from Elastic');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Updated name');
});
});
describe('customized field w/o an upgrade (ABA)', () => {
it('shows default (customization)', async () => {
const { diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Customized name',
target: 'Initial name',
merged: 'Customized name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate,
});
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('My changes');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Customized name');
});
it('shows resolved value', async () => {
const { fieldUpgradeWrapper, diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Customized name',
target: 'Initial name',
merged: 'Customized name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate,
});
switchToFieldEdit(fieldUpgradeWrapper);
await inputFieldValue(fieldUpgradeWrapper, { fieldName: 'name', value: 'Resolved name' });
await saveFieldValue(fieldUpgradeWrapper);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('My changes');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Resolved name');
});
it('shows the same diff after saving unchanged field value', async () => {
const { fieldUpgradeWrapper, diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Customized name',
target: 'Initial name',
merged: 'Customized name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate,
});
switchToFieldEdit(fieldUpgradeWrapper);
await saveFieldValue(fieldUpgradeWrapper);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('My changes');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Customized name');
});
});
describe('customized field w/ the matching upgrade (ABB)', () => {
it('shows default (customization)', async () => {
const { diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Updated name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate,
});
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('My changes and final updates');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Updated name');
});
it('shows incoming upgrade', async () => {
const { diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Updated name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate,
});
switchDiffViewTo(diffViewSelector, VersionsPickerOptionEnum.UpdateFromElastic);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('Changes from Elastic');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Updated name');
});
it('shows resolved value', async () => {
const { fieldUpgradeWrapper, diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Updated name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate,
});
switchToFieldEdit(fieldUpgradeWrapper);
await inputFieldValue(fieldUpgradeWrapper, { fieldName: 'name', value: 'Resolved name' });
await saveFieldValue(fieldUpgradeWrapper);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('My changes');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Resolved name');
});
it('shows the same diff after saving unchanged field value', async () => {
const { fieldUpgradeWrapper, diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Updated name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate,
});
switchToFieldEdit(fieldUpgradeWrapper);
await saveFieldValue(fieldUpgradeWrapper);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('My changes');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Updated name');
});
});
describe('customized field w/ an upgrade resulting in a solvable conflict (ABC)', () => {
it('shows default (merged value)', async () => {
const { diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Customized name',
target: 'Updated name',
merged: 'Merged name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent("My changes merged with Elastic's");
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Merged name');
});
it('shows incoming upgrade', async () => {
const { diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Customized name',
target: 'Updated name',
merged: 'Merged name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
switchDiffViewTo(diffViewSelector, VersionsPickerOptionEnum.UpdateFromElastic);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('Changes from Elastic');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Updated name');
});
it('shows original customization', async () => {
const { diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Customized name',
target: 'Updated name',
merged: 'Merged name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
switchDiffViewTo(diffViewSelector, VersionsPickerOptionEnum.MyOriginalChanges);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('My changes only');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Customized name');
});
it('shows resolved value', async () => {
const { fieldUpgradeWrapper, diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Customized name',
target: 'Updated name',
merged: 'Merged name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
switchToFieldEdit(fieldUpgradeWrapper);
await inputFieldValue(fieldUpgradeWrapper, { fieldName: 'name', value: 'Resolved name' });
await saveAndAcceptFieldValue(fieldUpgradeWrapper);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('My changes');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Resolved name');
});
it('shows the same diff after saving unchanged field value', async () => {
const { fieldUpgradeWrapper, diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Customized name',
target: 'Updated name',
merged: 'Merged name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
switchToFieldEdit(fieldUpgradeWrapper);
await saveAndAcceptFieldValue(fieldUpgradeWrapper);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent("My changes merged with Elastic's");
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Merged name');
});
});
describe('customized field w/ an upgrade resulting in a non-solvable conflict (ABC)', () => {
it('shows default diff view (customization)', async () => {
const { diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Customized name',
target: 'Updated name',
merged: 'Customized name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
});
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('My changes');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Customized name');
});
it('shows incoming upgrade', async () => {
const { diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Customized name',
target: 'Updated name',
merged: 'Customized name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
});
switchDiffViewTo(diffViewSelector, VersionsPickerOptionEnum.UpdateFromElastic);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('Changes from Elastic');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Updated name');
});
it('shows resolved value', async () => {
const { fieldUpgradeWrapper, diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Customized name',
target: 'Updated name',
merged: 'Customized name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
});
await inputFieldValue(fieldUpgradeWrapper, { fieldName: 'name', value: 'Resolved name' });
await saveAndAcceptFieldValue(fieldUpgradeWrapper);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('My changes');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Resolved name');
});
it('shows the same diff after saving unchanged field value', async () => {
const { fieldUpgradeWrapper, diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
base: 'Initial name',
current: 'Customized name',
target: 'Updated name',
merged: 'Customized name',
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
});
await saveAndAcceptFieldValue(fieldUpgradeWrapper);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('My changes');
expect(diffViewSection).toHaveTextContent('-Initial name');
expect(diffViewSection).toHaveTextContent('+Customized name');
});
});
describe('missing base - customized field w/ an upgrade resulting in a solvable conflict (-AB)', () => {
it('shows default diff view (incoming update)', async () => {
const { diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
current: 'Customized name',
target: 'Updated name',
merged: 'Customized name',
},
diffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('Changes from Elastic');
expect(diffViewSection).toHaveTextContent('-Customized name');
expect(diffViewSection).toHaveTextContent('+Updated name');
});
it('shows the same diff after saving unchanged field value', async () => {
const { fieldUpgradeWrapper, diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
current: 'Customized name',
target: 'Updated name',
merged: 'Customized name',
},
diffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
await acceptSuggestedFieldValue(fieldUpgradeWrapper);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('Changes from Elastic');
expect(diffViewSection).toHaveTextContent('-Customized name');
expect(diffViewSection).toHaveTextContent('+Updated name');
});
it('shows resolved value', async () => {
const { fieldUpgradeWrapper, diffViewSection, diffViewSelector } = await setup({
fieldVersions: {
current: 'Customized name',
target: 'Updated name',
merged: 'Customized name',
},
diffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
switchToFieldEdit(fieldUpgradeWrapper);
await inputFieldValue(fieldUpgradeWrapper, { fieldName: 'name', value: 'Resolved name' });
await saveAndAcceptFieldValue(fieldUpgradeWrapper);
expect(diffViewSelector).toBeVisible();
const selectedOption = within(diffViewSelector).getByRole('option', { selected: true });
expect(selectedOption).toHaveTextContent('My changes and final updates');
expect(diffViewSection).toHaveTextContent('-Customized name');
expect(diffViewSection).toHaveTextContent('+Resolved name');
});
});
});
interface SetupParams {
fieldVersions: {
base?: string;
current: string;
target: string;
merged: string;
};
diffOutcome: ThreeWayDiffOutcome;
conflict?: ThreeWayDiffConflict;
}
interface SetupResult {
fieldUpgradeWrapper: HTMLElement;
diffViewSection: HTMLElement;
diffViewSelector: HTMLElement;
}
async function setup({
fieldVersions,
diffOutcome,
conflict = ThreeWayDiffConflict.NONE,
}: SetupParams): Promise<SetupResult> {
mockRuleUpgradeReviewData({
ruleType: 'query',
fieldName: 'name',
fieldVersions,
diffOutcome,
conflict,
});
const { getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`name-upgradeWrapper`);
// Fields w/o conflicts are shown collapsed
if (conflict === ThreeWayDiffConflict.NONE) {
toggleFieldAccordion(fieldUpgradeWrapper);
}
const diffViewSection = within(fieldUpgradeWrapper).getByTestId(`name-comparisonSide`);
const diffViewSelector = within(diffViewSection).getByRole('combobox');
return { fieldUpgradeWrapper, diffViewSection, diffViewSelector };
}
function switchDiffViewTo(diffViewSelector: HTMLElement, option: VersionsPickerOptionEnum): void {
act(() => {
fireEvent.change(diffViewSelector, { target: { value: option } });
});
expect(
within(diffViewSelector).getByRole('option', {
selected: true,
})
).toHaveValue(option);
}

View file

@ -0,0 +1,18 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test/jest_integration',
rootDir: '../../../../../../../../../../../..',
modulePathIgnorePatterns: ['upgrade_rule_after_preview'],
roots: [
'<rootDir>/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management',
],
testMatch: ['**/*.test.[jt]s?(x)'],
openHandlesTimeout: 0,
forceExit: true,
};

View file

@ -0,0 +1,229 @@
/*
* 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 { screen, within } from '@testing-library/react';
import {
ThreeWayDiffConflict,
ThreeWayDiffOutcome,
} from '../../../../../../../common/api/detection_engine';
import {
mockRuleUpgradeReviewData,
renderRuleUpgradeFlyout,
} from './test_utils/rule_upgrade_flyout';
import {
switchToFieldEdit,
toggleFieldAccordion,
cancelFieldEdit,
saveFieldValue,
saveAndAcceptFieldValue,
} from './test_utils/rule_upgrade_helpers';
import { inputFieldValue } from './test_utils/set_field_value';
describe('Rule Upgrade button', () => {
describe('when there are no fields with conflicts', () => {
it('is enabled', async () => {
mockRuleUpgradeReviewData({
ruleType: 'query',
fieldName: 'name',
fieldVersions: {
base: 'Initial name',
current: 'Initial name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
await renderRuleUpgradeFlyout();
expectRuleUpgradeButtonToBeEnabled();
});
it('gets disabled after switching a field to edit mode', async () => {
mockRuleUpgradeReviewData({
ruleType: 'query',
fieldName: 'name',
fieldVersions: {
base: 'Initial name',
current: 'Initial name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`name-upgradeWrapper`);
toggleFieldAccordion(fieldUpgradeWrapper);
switchToFieldEdit(fieldUpgradeWrapper);
expectRuleUpgradeButtonToBeDisabled();
});
it('gets disabled when field value validation does not pass', async () => {
mockRuleUpgradeReviewData({
ruleType: 'query',
fieldName: 'name',
fieldVersions: {
base: 'Initial name',
current: 'Initial name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`name-upgradeWrapper`);
toggleFieldAccordion(fieldUpgradeWrapper);
switchToFieldEdit(fieldUpgradeWrapper);
await inputFieldValue(fieldUpgradeWrapper, { fieldName: 'name', value: '' });
expectRuleUpgradeButtonToBeDisabled();
});
it('gets enabled after switching to readonly mode', async () => {
mockRuleUpgradeReviewData({
ruleType: 'query',
fieldName: 'name',
fieldVersions: {
base: 'Initial name',
current: 'Initial name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`name-upgradeWrapper`);
toggleFieldAccordion(fieldUpgradeWrapper);
switchToFieldEdit(fieldUpgradeWrapper);
cancelFieldEdit(fieldUpgradeWrapper);
expectRuleUpgradeButtonToBeEnabled();
});
it('gets enabled after providing a resolved value', async () => {
mockRuleUpgradeReviewData({
ruleType: 'query',
fieldName: 'name',
fieldVersions: {
base: 'Initial name',
current: 'Initial name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`name-upgradeWrapper`);
toggleFieldAccordion(fieldUpgradeWrapper);
switchToFieldEdit(fieldUpgradeWrapper);
await inputFieldValue(fieldUpgradeWrapper, { fieldName: 'name', value: 'Resolved name' });
await saveFieldValue(fieldUpgradeWrapper);
expectRuleUpgradeButtonToBeEnabled();
});
});
describe('when there are fields with conflicts', () => {
it('is disabled with solvable conflict', async () => {
mockRuleUpgradeReviewData({
ruleType: 'query',
fieldName: 'name',
fieldVersions: {
base: 'Initial name',
current: 'Initial name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
await renderRuleUpgradeFlyout();
expectRuleUpgradeButtonToBeDisabled();
});
it('is disabled with non-solvable conflict', async () => {
mockRuleUpgradeReviewData({
ruleType: 'query',
fieldName: 'name',
fieldVersions: {
base: 'Initial name',
current: 'Initial name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
});
await renderRuleUpgradeFlyout();
expectRuleUpgradeButtonToBeDisabled();
});
it('gets enabled after providing a resolved value', async () => {
mockRuleUpgradeReviewData({
ruleType: 'query',
fieldName: 'name',
fieldVersions: {
base: 'Initial name',
current: 'Initial name',
target: 'Updated name',
merged: 'Updated name',
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
});
const { getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`name-upgradeWrapper`);
await inputFieldValue(fieldUpgradeWrapper, { fieldName: 'name', value: 'Resolved name' });
await saveAndAcceptFieldValue(fieldUpgradeWrapper);
expectRuleUpgradeButtonToBeEnabled();
});
});
});
function expectRuleUpgradeButtonToBeDisabled(): void {
expect(
within(screen.getByRole('dialog')).getByRole('button', {
name: 'Update rule',
})
).toBeDisabled();
}
function expectRuleUpgradeButtonToBeEnabled(): void {
expect(
within(screen.getByRole('dialog')).getByRole('button', {
name: 'Update rule',
})
).toBeEnabled();
}

View file

@ -0,0 +1,412 @@
/*
* 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 { act, fireEvent, within, waitFor } from '@testing-library/react';
import { isUndefined, omitBy } from 'lodash';
import {
PERFORM_RULE_UPGRADE_URL,
ThreeWayDiffConflict,
ThreeWayDiffOutcome,
} from '../../../../../../../../common/api/detection_engine';
import {
acceptSuggestedFieldValue,
saveFieldValue,
saveAndAcceptFieldValue,
switchToFieldEdit,
toggleFieldAccordion,
} from './rule_upgrade_helpers';
import { inputFieldValue } from './set_field_value';
import {
extractSingleKibanaFetchBodyBy,
mockRuleUpgradeReviewData,
renderRuleUpgradeFlyout,
} from './rule_upgrade_flyout';
interface AssertRuleUpgradeAfterReviewParams {
ruleType: string;
fieldName: string;
fieldVersions: {
initial: unknown;
customized: unknown;
upgrade: unknown;
resolvedValue: unknown;
};
}
export function assertRuleUpgradeAfterReview({
ruleType,
fieldName: rawFieldName,
fieldVersions: { initial, customized, upgrade, resolvedValue: rawResolvedValue },
}: AssertRuleUpgradeAfterReviewParams) {
// TS isn't able to infer the type of the field name for inputFieldValue()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolvedValue = rawResolvedValue as any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const fieldName = rawFieldName as any;
describe('non-customized field w/ an upgrade (AAB)', () => {
it('upgrades rule to merged value', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: initial,
target: upgrade,
merged: upgrade,
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByRole } = await renderRuleUpgradeFlyout();
await clickUpgradeRuleButton(getByRole('dialog'));
expectRuleUpgradeToMergedValue();
});
it('upgrades rule to resolved value', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: initial,
target: upgrade,
merged: upgrade,
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByRole, getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
toggleFieldAccordion(fieldUpgradeWrapper);
switchToFieldEdit(fieldUpgradeWrapper);
await inputFieldValue(fieldUpgradeWrapper, { fieldName, value: resolvedValue });
await saveFieldValue(fieldUpgradeWrapper);
await clickUpgradeRuleButton(getByRole('dialog'));
expectRuleUpgradeWithResolvedFieldValue(fieldName, resolvedValue);
});
});
describe('customized field w/o an upgrade (ABA)', () => {
it('upgrades rule to merged value', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: customized,
target: upgrade,
merged: customized,
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByRole } = await renderRuleUpgradeFlyout();
await clickUpgradeRuleButton(getByRole('dialog'));
expectRuleUpgradeToMergedValue();
});
it('upgrades rule to resolved value', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: customized,
target: upgrade,
merged: customized,
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByRole, getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
toggleFieldAccordion(fieldUpgradeWrapper);
switchToFieldEdit(fieldUpgradeWrapper);
await inputFieldValue(fieldUpgradeWrapper, {
fieldName,
value: resolvedValue,
});
await saveFieldValue(fieldUpgradeWrapper);
await clickUpgradeRuleButton(getByRole('dialog'));
expectRuleUpgradeWithResolvedFieldValue(fieldName, resolvedValue);
});
});
describe('customized field w/ the matching upgrade (ABB)', () => {
it('upgrades rule to merged value', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: upgrade,
target: upgrade,
merged: upgrade,
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByRole } = await renderRuleUpgradeFlyout();
await clickUpgradeRuleButton(getByRole('dialog'));
expectRuleUpgradeToMergedValue();
});
it('upgrades rule to resolved value', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: upgrade,
target: upgrade,
merged: upgrade,
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByRole, getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
toggleFieldAccordion(fieldUpgradeWrapper);
switchToFieldEdit(fieldUpgradeWrapper);
await inputFieldValue(fieldUpgradeWrapper, { fieldName, value: resolvedValue });
await saveFieldValue(fieldUpgradeWrapper);
await clickUpgradeRuleButton(getByRole('dialog'));
expectRuleUpgradeWithResolvedFieldValue(fieldName, resolvedValue);
});
});
describe('customized field w/ an upgrade resulting in a solvable conflict (ABC)', () => {
it('upgrades rule to suggested value', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: customized,
target: upgrade,
merged: customized,
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
const { getByRole, getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
await acceptSuggestedFieldValue(fieldUpgradeWrapper);
await clickUpgradeRuleButton(getByRole('dialog'));
expectRuleUpgradeWithResolvedFieldValue(fieldName, customized);
});
it('upgrades rule to resolved value', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: customized,
target: upgrade,
merged: customized,
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
const { getByRole, getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
switchToFieldEdit(fieldUpgradeWrapper);
await inputFieldValue(fieldUpgradeWrapper, { fieldName, value: resolvedValue });
await saveAndAcceptFieldValue(fieldUpgradeWrapper);
await clickUpgradeRuleButton(getByRole('dialog'));
expectRuleUpgradeWithResolvedFieldValue(fieldName, resolvedValue);
});
});
describe('customized field w/ an upgrade resulting in a non-solvable conflict (ABC)', () => {
it('upgrades rule to suggested value', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: customized,
target: upgrade,
merged: customized,
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
});
const { getByRole, getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
await saveAndAcceptFieldValue(fieldUpgradeWrapper);
await clickUpgradeRuleButton(getByRole('dialog'));
expectRuleUpgradeWithResolvedFieldValue(fieldName, customized);
});
it('upgrades rule to resolved value', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: customized,
target: upgrade,
merged: customized,
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
});
const { getByRole, getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
await inputFieldValue(fieldUpgradeWrapper, { fieldName, value: resolvedValue });
await saveAndAcceptFieldValue(fieldUpgradeWrapper);
await clickUpgradeRuleButton(getByRole('dialog'));
expectRuleUpgradeWithResolvedFieldValue(fieldName, resolvedValue);
});
});
describe('missing base version - customized field w/ an upgrade resulted in a solvable conflict (-AB)', () => {
it('upgrades rule to suggested value', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
current: customized,
target: upgrade,
merged: customized,
},
diffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
const { getByRole, getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
await acceptSuggestedFieldValue(fieldUpgradeWrapper);
await clickUpgradeRuleButton(getByRole('dialog'));
expectRuleUpgradeWithResolvedFieldValue(fieldName, customized);
});
it('upgrades rule to resolved value', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
current: customized,
target: upgrade,
merged: customized,
},
diffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
const { getByRole, getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
switchToFieldEdit(fieldUpgradeWrapper);
await inputFieldValue(fieldUpgradeWrapper, { fieldName, value: resolvedValue });
await saveAndAcceptFieldValue(fieldUpgradeWrapper);
await clickUpgradeRuleButton(getByRole('dialog'));
expectRuleUpgradeWithResolvedFieldValue(fieldName, resolvedValue);
});
});
}
async function clickUpgradeRuleButton(wrapper: HTMLElement): Promise<void> {
const upgradeRuleButton = within(wrapper).getByRole('button', {
name: 'Update rule',
});
expect(upgradeRuleButton).toBeVisible();
await waitFor(() => expect(upgradeRuleButton).toBeEnabled(), {
timeout: 500,
});
await act(async () => {
fireEvent.click(upgradeRuleButton);
});
}
function expectRuleUpgradeToMergedValue(): void {
const body = extractSingleKibanaFetchBodyBy({
path: PERFORM_RULE_UPGRADE_URL,
method: 'POST',
});
expect(body).toMatchObject({
mode: 'SPECIFIC_RULES',
rules: [{ rule_id: 'test-rule', revision: 1, fields: {} }],
pick_version: 'MERGED',
});
}
function expectRuleUpgradeWithResolvedFieldValue(fieldName: string, value: unknown): void {
const body = extractSingleKibanaFetchBodyBy({
path: PERFORM_RULE_UPGRADE_URL,
method: 'POST',
});
expect(body).toMatchObject({
mode: 'SPECIFIC_RULES',
rules: [
{
rule_id: 'test-rule',
revision: 1,
fields: {
[fieldName]: omitBy({ pick_version: 'RESOLVED', resolved_value: value }, isUndefined),
},
},
],
pick_version: 'MERGED',
});
}

View file

@ -0,0 +1,279 @@
/*
* 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 { within } from '@testing-library/react';
import {
ThreeWayDiffConflict,
ThreeWayDiffOutcome,
} from '../../../../../../../../common/api/detection_engine';
import { toggleFieldAccordion } from './rule_upgrade_helpers';
import { mockRuleUpgradeReviewData, renderRuleUpgradeFlyout } from './rule_upgrade_flyout';
interface AssertRuleUpgradePreviewParams {
ruleType: string;
fieldName: string;
humanizedFieldName: string;
fieldVersions: {
initial: unknown;
customized: unknown;
upgrade: unknown;
resolvedValue: unknown;
};
}
export function assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: { initial, customized, upgrade },
}: AssertRuleUpgradePreviewParams) {
describe('preview rule upgrade', () => {
it('previews non-customized field w/ an upgrade (AAB)', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: initial,
target: upgrade,
merged: upgrade,
},
diffOutcome: ThreeWayDiffOutcome.StockValueCanUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
expectFieldUpgradeState(fieldUpgradeWrapper, {
humanizedFieldName,
upgradeStateSummary: 'No conflicts',
upgradeStateBadge: 'Ready for update',
isModified: false,
});
toggleFieldAccordion(fieldUpgradeWrapper);
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-comparisonSide`)).toBeVisible();
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-finalSide`)).toBeVisible();
});
it('previews customized field w/o an upgrade (ABA)', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: customized,
target: upgrade,
merged: customized,
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
expectFieldUpgradeState(fieldUpgradeWrapper, {
humanizedFieldName,
upgradeStateSummary: 'No update',
isModified: true,
});
toggleFieldAccordion(fieldUpgradeWrapper);
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-comparisonSide`)).toBeVisible();
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-finalSide`)).toBeVisible();
});
it('previews customized field w/ the matching upgrade (ABB)', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: upgrade,
target: upgrade,
merged: upgrade,
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
expectFieldUpgradeState(fieldUpgradeWrapper, {
humanizedFieldName,
upgradeStateSummary: 'Matching update',
isModified: true,
});
toggleFieldAccordion(fieldUpgradeWrapper);
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-comparisonSide`)).toBeVisible();
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-finalSide`)).toBeVisible();
});
it('previews customized field w/ an upgrade resulting in a solvable conflict (ABC)', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: customized,
target: upgrade,
merged: customized,
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
const { getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
expectFieldUpgradeState(fieldUpgradeWrapper, {
humanizedFieldName,
upgradeStateSummary: 'Auto-resolved conflict',
upgradeStateBadge: 'Review required',
isModified: true,
});
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-comparisonSide`)).toBeVisible();
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-finalSide`)).toBeVisible();
});
it('previews customized field w/ an upgrade resulting in a non-solvable conflict (ABC)', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
base: initial,
current: customized,
target: upgrade,
merged: customized,
},
diffOutcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
conflict: ThreeWayDiffConflict.NON_SOLVABLE,
});
const { getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
expectFieldUpgradeState(fieldUpgradeWrapper, {
humanizedFieldName,
upgradeStateSummary: 'Unresolved conflict',
upgradeStateBadge: 'Action required',
isModified: true,
});
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-comparisonSide`)).toBeVisible();
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-finalSide`)).toBeVisible();
});
it('missing base - previews customized field w/ an upgrade and no conflict (-AB)', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
current: customized,
target: upgrade,
merged: customized,
},
diffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
conflict: ThreeWayDiffConflict.NONE,
});
const { getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
expectFieldUpgradeState(fieldUpgradeWrapper, {
humanizedFieldName,
upgradeStateSummary: 'No conflict',
upgradeStateBadge: 'Ready for update',
isModified: false,
});
toggleFieldAccordion(fieldUpgradeWrapper);
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-comparisonSide`)).toBeVisible();
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-finalSide`)).toBeVisible();
});
it('missing base - previews customized field w/ an upgrade resulting in a solvable conflict (-AB)', async () => {
mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions: {
current: customized,
target: upgrade,
merged: customized,
},
diffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
conflict: ThreeWayDiffConflict.SOLVABLE,
});
const { getByTestId } = await renderRuleUpgradeFlyout();
const fieldUpgradeWrapper = getByTestId(`${fieldName}-upgradeWrapper`);
expectFieldUpgradeState(fieldUpgradeWrapper, {
humanizedFieldName,
upgradeStateSummary: 'Auto-resolved conflict',
upgradeStateBadge: 'Review required',
isModified: false,
});
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-comparisonSide`)).toBeVisible();
expect(within(fieldUpgradeWrapper).getByTestId(`${fieldName}-finalSide`)).toBeVisible();
});
});
}
interface ExpectFieldUpgradeStateParams {
/**
* Human readable name shown in UI
*/
humanizedFieldName: string;
/**
* Field upgrade state summary text like "No conflict" or "Solvable conflict"
*/
upgradeStateSummary: string;
/**
* Field upgrade state badge text like "Ready to Update" and "Review required"
*/
upgradeStateBadge?: string;
/**
* Whether field's "Modified" badge is shown
*/
isModified: boolean;
}
function expectFieldUpgradeState(
wrapper: HTMLElement,
params: ExpectFieldUpgradeStateParams
): void {
expect(wrapper).toHaveTextContent(params.humanizedFieldName);
expect(wrapper).toHaveTextContent(params.upgradeStateSummary);
if (params.upgradeStateBadge) {
expect(within(wrapper).getByTitle(params.upgradeStateBadge)).toBeVisible();
}
if (params.isModified) {
expect(within(wrapper).getByTitle('Modified')).toBeVisible();
} else {
expect(within(wrapper).queryByTitle('Modified')).not.toBeInTheDocument();
}
}

View file

@ -0,0 +1,258 @@
/*
* 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, act, fireEvent, screen } from '@testing-library/react';
import type {
DataViewField,
DataViewFieldMap,
DataViewSpec,
FieldSpec,
} from '@kbn/data-views-plugin/common';
import { invariant } from '../../../../../../../../common/utils/invariant';
import { TIMELINES_URL } from '../../../../../../../../common/constants';
import { RulesPage } from '../../..';
import type { RelatedIntegration } from '../../../../../../../../common/api/detection_engine';
import {
GET_ALL_INTEGRATIONS_URL,
GET_PREBUILT_RULES_STATUS_URL,
REVIEW_RULE_UPGRADE_URL,
ThreeWayDiffConflict,
ThreeWayDiffOutcome,
ThreeWayMergeOutcome,
} from '../../../../../../../../common/api/detection_engine';
import { KibanaServices } from '../../../../../../../common/lib/kibana';
import { RuleUpgradeTestProviders } from './rule_upgrade_test_providers';
/** **********************************************/
// Mocks necessary to render Rule Upgrade Flyout
jest.mock('../../../../../../../detections/components/user_info');
jest.mock('../../../../../../../detections/containers/detection_engine/lists/use_lists_config');
/** **********************************************/
/**
* Stores KibanaServices.get().http.fetch() mocked responses.
*/
const mockedResponses = new Map<string, unknown>();
export async function renderRuleUpgradeFlyout(): Promise<ReturnType<typeof render>> {
// KibanaServices.get().http.fetch persists globally
// it's important to clear the state for the later assertions
(KibanaServices.get().http.fetch as jest.Mock).mockClear();
(KibanaServices.get().http.fetch as jest.Mock).mockImplementation((requestedPath) =>
mockedResponses.get(requestedPath)
);
mockKibanaFetchResponse(GET_PREBUILT_RULES_STATUS_URL, {
stats: {
num_prebuilt_rules_installed: 1,
num_prebuilt_rules_to_install: 0,
num_prebuilt_rules_to_upgrade: 1,
num_prebuilt_rules_total_in_package: 1,
},
});
const renderResult = render(<RulesPage />, {
wrapper: RuleUpgradeTestProviders,
});
await openRuleUpgradeFlyout();
return renderResult;
}
interface MockRuleUpgradeReviewDataParams {
ruleType: string;
fieldName: string;
fieldVersions: {
base?: unknown;
current: unknown;
target: unknown;
merged: unknown;
};
diffOutcome: ThreeWayDiffOutcome;
conflict: ThreeWayDiffConflict;
}
export function mockRuleUpgradeReviewData({
ruleType,
fieldName,
fieldVersions,
diffOutcome,
conflict,
}: MockRuleUpgradeReviewDataParams): void {
mockKibanaFetchResponse(REVIEW_RULE_UPGRADE_URL, {
stats: {
num_rules_to_upgrade_total: 1,
num_rules_with_conflicts:
conflict === ThreeWayDiffConflict.SOLVABLE || conflict === ThreeWayDiffConflict.NON_SOLVABLE
? 1
: 0,
num_rules_with_non_solvable_conflicts: conflict === ThreeWayDiffConflict.NON_SOLVABLE ? 1 : 0,
tags: [],
},
rules: [
{
id: 'test-rule',
rule_id: 'test-rule',
current_rule: {
rule_id: 'test-rule',
type: ruleType,
rule_source: {
type: 'external',
is_customized: true,
},
},
target_rule: {
rule_id: 'test-rule',
type: ruleType,
},
diff: {
num_fields_with_updates: 2, // tested field + version field
num_fields_with_conflicts: 1,
num_fields_with_non_solvable_conflicts: 1,
fields: {
[fieldName]: {
base_version: fieldVersions.base,
current_version: fieldVersions.current,
target_version: fieldVersions.target,
merged_version: fieldVersions.merged,
diff_outcome: diffOutcome,
merge_outcome: ThreeWayMergeOutcome.Current,
has_base_version: Boolean(fieldVersions.base),
has_update:
diffOutcome === ThreeWayDiffOutcome.CustomizedValueCanUpdate ||
diffOutcome === ThreeWayDiffOutcome.StockValueCanUpdate ||
diffOutcome === ThreeWayDiffOutcome.MissingBaseCanUpdate,
conflict,
},
},
},
revision: 1,
},
],
});
}
/**
*
* @param dataViews Mocked data views
* @param stickyFields Fields added to all data views obtained via `dataViews.create()` or `dataViews.get()`
*/
export function mockAvailableDataViews(
dataViews: DataViewSpec[],
stickyFields: DataViewFieldMap
): void {
(KibanaServices.get().data.dataViews.getIdsWithTitle as jest.Mock).mockResolvedValue(
dataViews.map(({ id, title }) => ({ id, title }))
);
(KibanaServices.get().data.dataViews.create as jest.Mock).mockImplementation((dataViewSpec) =>
createMockDataView({
...dataViewSpec,
fields: { ...(dataViewSpec.fields ?? {}), ...stickyFields },
})
);
(KibanaServices.get().data.dataViews.get as jest.Mock).mockImplementation((id: string) => {
const dataView = dataViews.find((dv) => dv.id === id);
invariant(
dataView,
`It's expected to have data view ${id} mock passed to mockAvailableDataViews() but it was not found`
);
return createMockDataView({
...dataView,
fields: { ...(dataView.fields ?? {}), ...stickyFields },
});
});
}
export function mockRelatedIntegrations(relatedIntegrations: RelatedIntegration[]): void {
mockKibanaFetchResponse(GET_ALL_INTEGRATIONS_URL, {
integrations: relatedIntegrations.map((ri) => ({
package_name: ri.package,
package_title: ri.package,
is_installed: true,
is_enabled: true,
latest_package_version: ri.version,
installed_package_version: ri.version,
integration_name: ri.integration,
integration_title: ri.integration,
})),
});
}
export function mockTimelines(timelines: Array<{ id: string; title: string }>): void {
mockKibanaFetchResponse(TIMELINES_URL, {
timeline: timelines.map((t, index) => ({
templateTimelineId: t.id,
title: t.title,
savedObjectId: `so-id-${index}`,
version: '1',
})),
totalCount: timelines.length,
});
}
/**
* Mocks KibanaServices.get().http.fetch() responses. Works in combination with renderRuleUpgradeFlyout.
*/
export function mockKibanaFetchResponse(path: string, mockResponse: unknown): void {
mockedResponses.set(path, mockResponse);
}
async function openRuleUpgradeFlyout(): Promise<void> {
await act(async () => {
fireEvent.click(await screen.findByTestId('ruleName'));
});
}
const createMockDataView = ({ id, title, fields }: DataViewSpec) =>
Promise.resolve({
id,
title,
fields: Object.values(fields ?? {}).map(createFieldDefinition),
getIndexPattern: jest.fn().mockReturnValue(title),
toSpec: jest.fn().mockReturnValue({
id,
title,
fields: Object.values(fields ?? {}).map(createFieldDefinition),
}),
});
function createFieldDefinition(fieldSpec: FieldSpec): Partial<DataViewField> {
return {
...fieldSpec,
spec: {
...fieldSpec,
},
};
}
interface ExtractKibanaFetchRequestByParams {
path: string;
method: string;
}
export function extractSingleKibanaFetchBodyBy({
path,
method,
}: ExtractKibanaFetchRequestByParams): Record<string, unknown> {
const kibanaFetchMock = KibanaServices.get().http.fetch as jest.Mock;
const ruleUpgradeRequests = kibanaFetchMock.mock.calls.filter(
([_path, _options]) => _path === path && _options.method === method
);
expect(ruleUpgradeRequests).toHaveLength(1);
try {
return JSON.parse(ruleUpgradeRequests[0][1].body);
} catch {
throw new Error('Unable to parse Kibana fetch body');
}
}

View file

@ -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 { act, fireEvent, waitFor, within } from '@testing-library/react';
export function toggleFieldAccordion(fieldWrapper: HTMLElement): void {
act(() => {
const accordionButton = within(fieldWrapper).getAllByRole('button')[0];
fireEvent.click(accordionButton);
});
}
export function switchToFieldEdit(wrapper: HTMLElement): void {
act(() => {
fireEvent.click(within(wrapper).getByRole('button', { name: 'Edit' }));
});
}
export function cancelFieldEdit(wrapper: HTMLElement): void {
act(() => {
fireEvent.click(within(wrapper).getByRole('button', { name: 'Cancel' }));
});
}
export async function acceptSuggestedFieldValue(wrapper: HTMLElement): Promise<void> {
await act(async () => {
fireEvent.click(within(wrapper).getByRole('button', { name: 'Accept' }));
});
}
export async function saveFieldValue(wrapper: HTMLElement): Promise<void> {
await clickFieldSaveButton(wrapper, 'Save');
}
export async function saveAndAcceptFieldValue(wrapper: HTMLElement): Promise<void> {
await clickFieldSaveButton(wrapper, 'Save and accept');
}
async function clickFieldSaveButton(wrapper: HTMLElement, buttonName: string): Promise<void> {
const saveButton = within(wrapper).getByRole('button', { name: buttonName });
expect(saveButton).toBeVisible();
// Wait for async validation to finish
await waitFor(() => expect(saveButton).toBeEnabled(), {
timeout: 500,
});
await act(async () => {
fireEvent.click(saveButton);
});
// After saving the form "Save" button should be removed from the DOM
await waitFor(() => expect(saveButton).not.toBeInTheDocument(), {
timeout: 500,
});
}

View file

@ -0,0 +1,97 @@
/*
* 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 { PropsWithChildren } from 'react';
import { EuiProvider } from '@elastic/eui';
import { I18nProvider } from '@kbn/i18n-react';
import { euiDarkVars } from '@kbn/ui-theme';
import { ThemeProvider } from 'styled-components';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { SecurityPageName } from '@kbn/deeplinks-security';
import { KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary';
import { MemoryRouter } from 'react-router-dom';
import { MockDiscoverInTimelineContext } from '../../../../../../../common/components/discover_in_timeline/mocks/discover_in_timeline_provider';
import { createKibanaContextProviderMock } from '../../../../../../../common/lib/kibana/kibana_react.mock';
import { createMockStore } from '../../../../../../../common/mock';
import { RouterSpyStateContext } from '../../../../../../../common/utils/route/helpers';
import { AllRulesTabs } from '../../../../../components/rules_table/rules_table_toolbar';
import { useKibana } from '../../../../../../../common/lib/kibana';
import { MlCapabilitiesProvider } from '../../../../../../../common/components/ml/permissions/ml_capabilities_provider';
import { UpsellingProvider } from '../../../../../../../common/components/upselling_provider';
const MockKibanaContextProvider = createKibanaContextProviderMock();
function UpsellingProviderMock({ children }: React.PropsWithChildren<{}>): JSX.Element {
return (
<UpsellingProvider upsellingService={useKibana().services.upselling}>
{children}
</UpsellingProvider>
);
}
/**
* Defining custom Test Providers for Rule Upgrade Flyout required to avoid
* impact on existing tests. Existing TestProviders doesn't provide necessary
* contexts for the Rule Upgrade Flyout like `MlCapabilitiesProvider`. Mocking the
* latter in TestProviders requires to refactor multiple Jest tests due to
* `useKibana()` custom mocks also used in `MlCapabilitiesProvider`.
*/
export function RuleUpgradeTestProviders({ children }: PropsWithChildren<{}>): JSX.Element {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
logger: {
log: jest.fn(),
warn: jest.fn(),
error: () => {},
},
});
const store = createMockStore();
return (
<KibanaErrorBoundaryProvider analytics={undefined}>
<RouterSpyStateContext.Provider
value={[
{
pageName: SecurityPageName.rules,
detailName: undefined,
tabName: AllRulesTabs.updates,
search: '',
pathName: '/',
state: undefined,
},
jest.fn(),
]}
>
<MemoryRouter>
<MockKibanaContextProvider>
<MlCapabilitiesProvider>
<I18nProvider>
<UpsellingProviderMock>
<ReduxStoreProvider store={store}>
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<QueryClientProvider client={queryClient}>
<MockDiscoverInTimelineContext>
<EuiProvider highContrastMode={false}>{children}</EuiProvider>
</MockDiscoverInTimelineContext>
</QueryClientProvider>
</ThemeProvider>
</ReduxStoreProvider>
</UpsellingProviderMock>
</I18nProvider>
</MlCapabilitiesProvider>
</MockKibanaContextProvider>
</MemoryRouter>
</RouterSpyStateContext.Provider>
</KibanaErrorBoundaryProvider>
);
}

View file

@ -0,0 +1,845 @@
/*
* 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 { act, fireEvent, within, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TimeDuration } from '@kbn/securitysolution-utils/time_duration';
import { invariant } from '../../../../../../../../common/utils/invariant';
import { toSimpleRuleSchedule } from '../../../../../../../../common/api/detection_engine/model/rule_schema/to_simple_rule_schedule';
import {
addEuiComboBoxOption,
clearEuiComboBoxSelection,
selectEuiComboBoxOption,
} from '../../../../../../../common/test/eui/combobox';
import { selectEuiSuperSelectOption } from '../../../../../../../common/test/eui/super_select';
import type {
AlertSuppression,
AnomalyThreshold,
HistoryWindowStart,
InlineKqlQuery,
MachineLearningJobId,
NewTermsFields,
RuleEqlQuery,
RuleKqlQuery,
ThreatIndex,
Threshold,
} from '../../../../../../../../common/api/detection_engine';
import {
DataSourceType,
type BuildingBlockObject,
type DiffableAllFields,
type InvestigationFields,
type RelatedIntegration,
type RequiredField,
type RiskScoreMapping,
type RuleDataSource,
type RuleNameOverrideObject,
type SeverityMapping,
type Threat,
type TimelineTemplateReference,
type TimestampOverrideObject,
KqlQueryType,
} from '../../../../../../../../common/api/detection_engine';
import type { RuleSchedule } from '../../../../../../../../common/api/detection_engine/model/rule_schema/rule_schedule';
type ToDiscriminatedUnion<T> = {
[K in keyof T]-?: { fieldName: K; value: T[K] };
}[keyof T];
export async function inputFieldValue(
wrapper: HTMLElement,
params: ToDiscriminatedUnion<DiffableAllFields>
): Promise<void> {
const fieldFinalSide = within(wrapper).getByTestId(`${params.fieldName}-finalSide`);
switch (params.fieldName) {
case 'name':
await inputText(fieldFinalSide, params.value);
break;
case 'description':
await inputText(fieldFinalSide, params.value);
break;
case 'severity':
await inputSeverity(fieldFinalSide, params.value);
break;
case 'severity_mapping':
await inputSeverityMapping(fieldFinalSide, params.value);
break;
case 'risk_score':
await inputRiskScore(fieldFinalSide, params.value);
break;
case 'risk_score_mapping':
await inputRiskScoreOverride(fieldFinalSide, params.value);
break;
case 'references':
await inputStringsArray(fieldFinalSide, {
addInputButtonName: 'Add reference URL',
items: params.value,
});
break;
case 'false_positives':
await inputStringsArray(fieldFinalSide, {
addInputButtonName: 'Add false positive example',
items: params.value,
});
break;
case 'threat':
await inputThreat(fieldFinalSide, params.value);
break;
case 'note':
await inputText(fieldFinalSide, params.value);
break;
case 'setup':
await inputText(fieldFinalSide, params.value);
break;
case 'related_integrations':
await inputRelatedIntegrations(fieldFinalSide, params.value);
break;
case 'required_fields':
await inputRequiredFields(fieldFinalSide, params.value);
break;
case 'rule_schedule':
await inputRuleSchedule(fieldFinalSide, params.value);
break;
case 'max_signals':
await inputMaxSignals(fieldFinalSide, params.value);
break;
case 'rule_name_override':
await inputRuleNameOverride(fieldFinalSide, params.value);
break;
case 'timestamp_override':
await inputTimestampOverride(fieldFinalSide, params.value);
break;
case 'timeline_template':
await inputTimelineTemplate(fieldFinalSide, params.value);
break;
case 'building_block':
await inputBuildingBlock(fieldFinalSide, params.value);
break;
case 'investigation_fields':
await inputInvestigationFields(fieldFinalSide, params.value);
break;
case 'data_source':
await inputDataSource(fieldFinalSide, params.value);
break;
case 'alert_suppression':
await inputAlertSuppression(fieldFinalSide, params.value);
break;
case 'anomaly_threshold':
await inputAnomalyThreshold(fieldFinalSide, params.value);
break;
case 'kql_query':
await inputKqlQuery(fieldFinalSide, params.value);
break;
case 'eql_query':
await inputEqlQuery(fieldFinalSide, params.value);
break;
case 'esql_query':
throw new Error('Not implemented');
case 'history_window_start':
await inputHistoryWindowStart(fieldFinalSide, params.value);
break;
case 'machine_learning_job_id':
await inputMachineLearningJobId(fieldFinalSide, params.value);
break;
case 'new_terms_fields':
await inputNewTermsFields(fieldFinalSide, params.value);
break;
case 'threat_index':
await inputThreatIndex(fieldFinalSide, params.value);
break;
case 'threat_indicator_path':
await inputText(fieldFinalSide, params.value ?? '');
break;
case 'threat_mapping':
throw new Error('Not implemented');
case 'threat_query':
await inputThreatQuery(fieldFinalSide, params.value);
break;
case 'threshold':
await inputThreshold(fieldFinalSide, params.value);
break;
}
}
async function fireEnterEvent(el: HTMLElement): Promise<void> {
await act(async () => {
el.focus();
await userEvent.keyboard('{Enter}');
});
}
async function inputText(fieldFinalSide: HTMLElement, value: string): Promise<void> {
await act(async () => {
const input = within(fieldFinalSide).getByRole('textbox');
fireEvent.change(input, {
target: { value },
});
});
}
async function inputSeverity(fieldFinalSide: HTMLElement, value: string): Promise<void> {
const toggleButton = within(fieldFinalSide).getByTestId('select');
await selectEuiSuperSelectOption({
toggleButton,
optionText: value,
});
}
async function inputSeverityMapping(
fieldFinalSide: HTMLElement,
value: SeverityMapping
): Promise<void> {
const severityArray = ['low', 'medium', 'high', 'critical'];
const severityMappingFormRows = within(fieldFinalSide).getAllByTestId('severityOverrideRow');
expect(severityMappingFormRows).toHaveLength(severityArray.length);
for (let i = 0; i < severityArray.length; ++i) {
const severityLevel = severityArray[i];
const formRow = severityMappingFormRows[i];
const [sourceFieldComboboxInput, sourceValueComboboxInput] =
within(formRow).getAllByRole('combobox');
const mapping = value.find((x) => x.severity.toLowerCase() === severityLevel);
if (mapping) {
await act(async () => {
fireEvent.change(sourceFieldComboboxInput, {
target: { value: mapping.field },
});
});
await fireEnterEvent(sourceFieldComboboxInput);
await act(async () => {
fireEvent.change(sourceValueComboboxInput, {
target: { value: mapping.value },
});
});
await fireEnterEvent(sourceValueComboboxInput);
} else {
// Clear mapping value for the current severity level
await act(async () => {
sourceFieldComboboxInput.focus();
await userEvent.keyboard('{Backspace}');
});
}
}
}
async function inputRiskScore(fieldFinalSide: HTMLElement, value: number): Promise<void> {
await act(async () => {
// EuiRange is used for Risk Score
const [riskScoreInput] = within(fieldFinalSide).getAllByTestId(
'defaultRiskScore-defaultRiskRange'
);
fireEvent.change(riskScoreInput, {
target: { value },
});
});
}
async function inputRiskScoreOverride(
fieldFinalSide: HTMLElement,
value: RiskScoreMapping
): Promise<void> {
invariant(value.length === 1, 'setRiskScoreOverride() expects a single entry risk score mapping');
const sourceFieldComboboxInput = within(fieldFinalSide).getByRole('combobox');
await waitFor(() => expect(sourceFieldComboboxInput).toBeEnabled(), { timeout: 500 });
await act(async () => {
fireEvent.change(sourceFieldComboboxInput, {
target: { value: value[0].field },
});
});
await fireEnterEvent(sourceFieldComboboxInput);
}
async function inputStringsArray(
fieldFinalSide: HTMLElement,
{
addInputButtonName,
items,
}: {
addInputButtonName: string;
items: string[];
}
): Promise<void> {
await removeExistingItems(fieldFinalSide);
const addItem = async () => {
await act(async () => {
fireEvent.click(
within(fieldFinalSide).getByRole('button', {
name: addInputButtonName,
})
);
});
};
for (let i = 0; i < items.length; ++i) {
await addItem();
const inputs = within(fieldFinalSide).getAllByRole('textbox');
await act(async () => {
fireEvent.change(inputs[i], {
target: { value: items[i] },
});
});
}
}
// Limited to tactics
async function inputThreat(fieldFinalSide: HTMLElement, value: Threat[]): Promise<void> {
await removeExistingItems(fieldFinalSide);
const addTactic = async () => {
await act(async () => {
fireEvent.click(
within(fieldFinalSide).getByRole('button', {
name: 'Add tactic',
})
);
});
};
for (let i = 0; i < value.length; ++i) {
await addTactic();
await selectEuiSuperSelectOption({
toggleButton: within(fieldFinalSide).getAllByTestId('mitreAttackTactic')[i],
optionText: `${value[i].tactic.name} (${value[i].tactic.id})`,
});
}
}
/**
* Requires mocking response with integrations from `GET /internal/detection_engine/fleet/integrations/all`
*/
async function inputRelatedIntegrations(
fieldFinalSide: HTMLElement,
value: RelatedIntegration[]
): Promise<void> {
await removeExistingItems(fieldFinalSide, { removeButtonName: 'Remove related integration' });
const addIntegration = async () => {
await act(async () => {
fireEvent.click(
within(fieldFinalSide).getByRole('button', {
name: 'Add integration',
})
);
});
};
for (let i = 0; i < value.length; ++i) {
const { package: integrationPackageName, version } = value[i];
await addIntegration();
await selectEuiComboBoxOption({
comboBoxToggleButton: within(fieldFinalSide).getAllByTestId('comboBoxToggleListButton')[i],
// Expect only installed and enabled integrations
optionText: `${integrationPackageName}Installed: Enabled`,
});
const packageVersionInput = within(fieldFinalSide).getAllByRole('textbox')[i];
await waitFor(() => expect(packageVersionInput).toBeEnabled(), { timeout: 500 });
await act(async () => {
fireEvent.change(packageVersionInput, {
target: { value: version },
});
});
}
}
async function inputRequiredFields(
fieldFinalSide: HTMLElement,
value: RequiredField[]
): Promise<void> {
await removeExistingItems(fieldFinalSide, { removeButtonName: 'Remove required field' });
const addRequiredField = async () => {
await act(async () => {
fireEvent.click(
within(fieldFinalSide).getByRole('button', {
name: 'Add required field',
})
);
});
};
for (let i = 0; i < value.length; ++i) {
const { name, type } = value[i];
await addRequiredField();
const formRow = within(fieldFinalSide).getAllByTestId('requiredFieldsFormRow')[i];
const [nameInput, typeInput] = within(formRow).getAllByRole('combobox');
await act(async () => {
fireEvent.change(nameInput, {
target: { value: name },
});
});
await fireEnterEvent(nameInput);
await act(async () => {
fireEvent.change(typeInput, {
target: { value: type },
});
});
await fireEnterEvent(typeInput);
}
}
async function inputRuleSchedule(
fieldFinalSide: HTMLElement,
ruleSchedule: RuleSchedule
): Promise<void> {
const intervalFormRow = within(fieldFinalSide).getByTestId('intervalFormRow');
const intervalValueInput = within(intervalFormRow).getByRole('spinbutton');
const intervalUnitInput = within(intervalFormRow).getByRole('combobox');
const lookBackFormRow = within(fieldFinalSide).getByTestId('lookbackFormRow');
const lookBackValueInput = within(lookBackFormRow).getByRole('spinbutton');
const lookBackUnitInput = within(lookBackFormRow).getByRole('combobox');
const simpleRuleSchedule = toSimpleRuleSchedule(ruleSchedule);
invariant(
simpleRuleSchedule,
'Provided rule schedule is not convertible to simple rule schedule'
);
const parsedInterval = TimeDuration.parse(simpleRuleSchedule.interval);
const parsedLookBack = TimeDuration.parse(simpleRuleSchedule.lookback);
await act(async () => {
fireEvent.change(intervalValueInput, {
target: { value: parsedInterval?.value },
});
});
await act(async () => {
fireEvent.change(intervalUnitInput, {
target: { value: parsedInterval?.unit },
});
});
await act(async () => {
fireEvent.change(lookBackValueInput, {
target: { value: parsedLookBack?.value },
});
});
await act(async () => {
fireEvent.change(lookBackUnitInput, {
target: { value: parsedLookBack?.unit },
});
});
}
async function inputMaxSignals(fieldFinalSide: HTMLElement, value: number): Promise<void> {
const input = within(fieldFinalSide).getByRole('spinbutton');
await act(async () => {
fireEvent.change(input, {
target: { value },
});
});
}
async function inputRuleNameOverride(
fieldFinalSide: HTMLElement,
value: RuleNameOverrideObject | undefined
): Promise<void> {
await waitFor(
() => expect(within(fieldFinalSide).getByTestId('comboBoxSearchInput')).toBeEnabled(),
{ timeout: 500 }
);
if (value) {
await selectEuiComboBoxOption({
comboBoxToggleButton: within(fieldFinalSide).getByTestId('comboBoxToggleListButton'),
optionText: value.field_name,
});
} else {
await act(async () => {
within(fieldFinalSide).getByTestId('comboBoxSearchInput').focus();
await userEvent.keyboard('{Backspace}');
});
}
}
async function inputTimestampOverride(
fieldFinalSide: HTMLElement,
value: TimestampOverrideObject | undefined
): Promise<void> {
await waitFor(
() => expect(within(fieldFinalSide).getByTestId('comboBoxSearchInput')).toBeEnabled(),
{ timeout: 500 }
);
if (value) {
await selectEuiComboBoxOption({
comboBoxToggleButton: within(fieldFinalSide).getByTestId('comboBoxToggleListButton'),
optionText: value.field_name,
});
} else {
await act(async () => {
within(fieldFinalSide).getByTestId('comboBoxSearchInput').focus();
await userEvent.keyboard('{Backspace}');
});
}
}
async function inputTimelineTemplate(
fieldFinalSide: HTMLElement,
value: TimelineTemplateReference | undefined
): Promise<void> {
const timelineSelectToggleButton = within(fieldFinalSide).getByRole('combobox');
await act(async () => {
fireEvent.click(timelineSelectToggleButton);
});
const options = Array.from(document.querySelectorAll('[role="option"]'));
const lowerCaseOptionText = value?.timeline_title.toLocaleLowerCase() ?? 'None';
const optionToSelect = options.find((option) =>
option.textContent?.toLowerCase().includes(lowerCaseOptionText)
);
if (optionToSelect) {
await act(async () => {
fireEvent.click(optionToSelect);
});
} else {
throw new Error(
`Could not find option with text "${lowerCaseOptionText}". Available options: ${options
.map((option) => option.textContent)
.join(', ')}`
);
}
}
async function inputBuildingBlock(
fieldFinalSide: HTMLElement,
value: BuildingBlockObject | undefined
): Promise<void> {
const markGeneratedAlertsAsBuildingBlockAlertsCheckbox = within(fieldFinalSide).getByRole(
'checkbox'
) as HTMLInputElement;
// Field is already in the expected state, exit.
if (
(markGeneratedAlertsAsBuildingBlockAlertsCheckbox.checked && value) ||
(!markGeneratedAlertsAsBuildingBlockAlertsCheckbox.checked && !value)
) {
return;
}
await act(async () => {
fireEvent.click(markGeneratedAlertsAsBuildingBlockAlertsCheckbox);
});
}
async function inputInvestigationFields(
fieldFinalSide: HTMLElement,
value: InvestigationFields | undefined
): Promise<void> {
await waitFor(() =>
expect(within(fieldFinalSide).queryByTestId('comboBoxClearButton')).toBeVisible()
);
await clearEuiComboBoxSelection({
clearButton: within(fieldFinalSide).getByTestId('comboBoxClearButton'),
});
for (const fieldName of value?.field_names ?? []) {
await selectEuiComboBoxOption({
comboBoxToggleButton: within(fieldFinalSide).getByTestId('comboBoxToggleListButton'),
optionText: fieldName,
});
}
}
async function inputDataSource(
fieldFinalSide: HTMLElement,
dataSource: RuleDataSource | undefined
): Promise<void> {
if (!dataSource) {
return;
}
const indexPatternsEditWrapper = within(fieldFinalSide).getByTestId('indexPatternEdit');
const dataViewEditWrapper = within(fieldFinalSide).getByTestId('pick-rule-data-source');
switch (dataSource.type) {
case DataSourceType.index_patterns:
await clearEuiComboBoxSelection({
clearButton: within(indexPatternsEditWrapper).getByTestId('comboBoxClearButton'),
});
for (const indexPattern of dataSource.index_patterns) {
await addEuiComboBoxOption({
wrapper: indexPatternsEditWrapper,
optionText: indexPattern,
});
}
break;
case DataSourceType.data_view:
await waitFor(
() =>
expect(
within(dataViewEditWrapper).queryByTestId('comboBoxToggleListButton')
).toBeVisible(),
{
timeout: 500,
}
);
await selectEuiComboBoxOption({
comboBoxToggleButton: within(dataViewEditWrapper).getByTestId('comboBoxToggleListButton'),
optionText: dataSource.data_view_id,
});
break;
}
}
/**
* Implements only suppression fields
*/
async function inputAlertSuppression(
fieldFinalSide: HTMLElement,
value: AlertSuppression | undefined
): Promise<void> {
await clearEuiComboBoxSelection({
clearButton: within(fieldFinalSide).getByTestId('comboBoxClearButton'),
});
if (!value) {
return;
}
for (const fieldName of value.group_by) {
await selectEuiComboBoxOption({
comboBoxToggleButton: within(fieldFinalSide).getByTestId('comboBoxToggleListButton'),
optionText: fieldName,
});
}
}
async function inputAnomalyThreshold(
fieldFinalSide: HTMLElement,
value: AnomalyThreshold
): Promise<void> {
await act(async () => {
// EuiRange is used for anomaly threshold
const [riskScoreInput] = within(fieldFinalSide).getAllByTestId('anomalyThresholdRange');
fireEvent.change(riskScoreInput, {
target: { value },
});
});
}
/**
* Doesn't support filters and saved queries
*/
async function inputKqlQuery(fieldFinalSide: HTMLElement, value: RuleKqlQuery): Promise<void> {
if (value.type !== KqlQueryType.inline_query) {
return;
}
await waitFor(() => expect(within(fieldFinalSide).getByRole('textbox')).toBeVisible(), {
timeout: 500,
});
await inputText(fieldFinalSide, value.query);
}
/**
* Doesn't support filters and EQL options
*/
async function inputEqlQuery(fieldFinalSide: HTMLElement, value: RuleEqlQuery): Promise<void> {
await waitFor(() => expect(within(fieldFinalSide).getByRole('textbox')).toBeVisible(), {
timeout: 500,
});
await inputText(fieldFinalSide, value.query);
}
async function inputHistoryWindowStart(
fieldFinalSide: HTMLElement,
value: HistoryWindowStart
): Promise<void> {
const valueInput = within(fieldFinalSide).getByTestId('interval');
const unitInput = within(fieldFinalSide).getByTestId('timeType');
invariant(value.startsWith('now-'), 'Unable to parse history window start value');
const parsed = TimeDuration.parse(value.substring(4));
invariant(parsed, 'Unable to parse history window start value');
await act(async () => {
fireEvent.change(valueInput, {
target: { value: parsed.value },
});
});
await act(async () => {
fireEvent.change(unitInput, {
target: { value: parsed.unit },
});
});
}
async function inputMachineLearningJobId(
fieldFinalSide: HTMLElement,
value: MachineLearningJobId
): Promise<void> {
const jobIds = [value].flat();
await clearEuiComboBoxSelection({
clearButton: within(fieldFinalSide).getByTestId('comboBoxClearButton'),
});
for (const jobId of jobIds) {
await selectEuiComboBoxOption({
comboBoxToggleButton: within(fieldFinalSide).getByTestId('comboBoxToggleListButton'),
optionText: jobId,
});
}
}
async function inputNewTermsFields(
fieldFinalSide: HTMLElement,
value: NewTermsFields
): Promise<void> {
await clearEuiComboBoxSelection({
clearButton: within(fieldFinalSide).getByTestId('comboBoxClearButton'),
});
for (const fieldName of value) {
await selectEuiComboBoxOption({
comboBoxToggleButton: within(fieldFinalSide).getByTestId('comboBoxToggleListButton'),
optionText: fieldName,
});
}
}
async function inputThreatIndex(fieldFinalSide: HTMLElement, value: ThreatIndex): Promise<void> {
await clearEuiComboBoxSelection({
clearButton: within(fieldFinalSide).getByTestId('comboBoxClearButton'),
});
for (const indexPattern of value) {
await addEuiComboBoxOption({
wrapper: fieldFinalSide,
optionText: indexPattern,
});
}
}
/**
* Doesn't support filters
*/
async function inputThreatQuery(fieldFinalSide: HTMLElement, value: InlineKqlQuery): Promise<void> {
await waitFor(() => expect(within(fieldFinalSide).getByRole('textbox')).toBeVisible(), {
timeout: 500,
});
await inputText(fieldFinalSide, value.query);
}
async function inputThreshold(fieldFinalSide: HTMLElement, value: Threshold): Promise<void> {
const groupByFieldsComboBox = within(fieldFinalSide).getByTestId(
'detectionEngineStepDefineRuleThresholdField'
);
const thresholdInput = within(fieldFinalSide).getByTestId(
'detectionEngineStepDefineRuleThresholdValue'
);
await act(async () => {
const input = within(thresholdInput).getByRole('spinbutton');
fireEvent.change(input, {
target: { value: value.value },
});
});
const fields = [value.field].flat();
await clearEuiComboBoxSelection({
clearButton: within(groupByFieldsComboBox).getByTestId('comboBoxClearButton'),
});
for (const field of fields) {
await selectEuiComboBoxOption({
comboBoxToggleButton: within(groupByFieldsComboBox).getByTestId('comboBoxToggleListButton'),
optionText: field,
});
}
}
async function removeExistingItems(
wrapper: HTMLElement,
{ removeButtonName }: { removeButtonName: string } = { removeButtonName: 'Delete' }
): Promise<void> {
const deleteButtons = within(wrapper).getAllByRole('button', { name: removeButtonName });
for (let i = deleteButtons.length - 1; i >= 0; --i) {
await act(async () => {
fireEvent.click(deleteButtons[i]);
});
}
}

View file

@ -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 { mockAvailableDataViews } from '../../test_utils/rule_upgrade_flyout';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "alert_suppression" (query rule type) after preview in flyout', () => {
beforeAll(() => {
mockAvailableDataViews([], {
resolvedString: {
name: 'resolvedStringField',
type: 'string',
searchable: true,
aggregatable: true,
},
});
});
const ruleType = 'query';
const fieldName = 'alert_suppression';
const humanizedFieldName = 'Alert suppression';
const initial = { group_by: ['fieldA'] };
const customized = { group_by: ['fieldB'] };
const upgrade = { group_by: ['fieldC'] };
const resolvedValue = { group_by: ['resolvedStringField'] };
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "building_block" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'building_block';
const humanizedFieldName = 'Building Block';
const initial = undefined;
const customized = { type: 'default' };
const upgrade = { type: 'default' };
const resolvedValue = undefined;
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,75 @@
/*
* 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 { DataSourceType } from '../../../../../../../../../common/api/detection_engine';
import { mockAvailableDataViews } from '../../test_utils/rule_upgrade_flyout';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "data_source" (query rule type) after preview in flyout', () => {
beforeAll(() => {
mockAvailableDataViews(
[
{
id: 'resolved',
title: 'resolved',
},
{
id: 'data_view_B',
title: 'Data View B',
},
{
id: 'data_view_C',
title: 'Data View C',
},
],
{}
);
});
const ruleType = 'query';
const fieldName = 'data_source';
const humanizedFieldName = 'Data source';
describe.each([
{
initial: { type: DataSourceType.index_patterns, index_patterns: ['indexA'] },
customized: { type: DataSourceType.index_patterns, index_patterns: ['indexB'] },
upgrade: { type: DataSourceType.index_patterns, index_patterns: ['indexC'] },
resolvedValue: { type: DataSourceType.index_patterns, index_patterns: ['resolved'] },
},
{
initial: { type: DataSourceType.data_view, data_view_id: 'data_view_A' },
customized: { type: DataSourceType.data_view, data_view_id: 'data_view_B' },
upgrade: { type: DataSourceType.data_view, data_view_id: 'data_view_C' },
resolvedValue: { type: DataSourceType.data_view, data_view_id: 'resolved' },
},
] as const)('$resolvedValue.type', ({ initial, customized, upgrade, resolvedValue }) => {
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});
});

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "description" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'description';
const humanizedFieldName = 'Description';
const initial = 'Initial description';
const customized = 'Custom description';
const upgrade = 'Updated description';
const resolvedValue = 'Resolved description';
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "false_positives" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'false_positives';
const humanizedFieldName = 'False Positives';
const initial = ['exampleA'];
const customized = ['exampleB'];
const upgrade = ['exampleC'];
const resolvedValue = ['resolved'];
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { mockAvailableDataViews } from '../../test_utils/rule_upgrade_flyout';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "investigation_fields" (query rule type) after preview in flyout', () => {
beforeAll(() => {
mockAvailableDataViews([], {
resolvedString: {
name: 'resolvedStringField',
type: 'string',
searchable: true,
aggregatable: true,
},
});
});
const ruleType = 'query';
const fieldName = 'investigation_fields';
const humanizedFieldName = 'Custom highlighted fields';
const initial = { field_names: ['fieldA'] };
const customized = { field_names: ['fieldB'] };
const upgrade = { field_names: ['fieldC'] };
const resolvedValue = { field_names: ['resolvedStringField'] };
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,17 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test/jest_integration',
rootDir: '../../../../../../../../../../../../../..',
roots: [
'<rootDir>/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management',
],
testMatch: ['**/common_fields/*.test.[jt]s?(x)'],
openHandlesTimeout: 0,
forceExit: true,
};

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "max_signals" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'max_signals';
const humanizedFieldName = 'Max Signals';
const initial = 100;
const customized = 150;
const upgrade = 200;
const resolvedValue = 300;
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "name" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'name';
const humanizedFieldName = 'Name';
const initial = 'Initial name';
const customized = 'Custom name';
const upgrade = 'Updated name';
const resolvedValue = 'Resolved name';
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "note" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'note';
const humanizedFieldName = 'Investigation guide';
const initial = 'Initial investigation guide';
const customized = 'Custom investigation guide';
const upgrade = 'Updated investigation guide';
const resolvedValue = 'resolved investigation guide';
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "references" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'references';
const humanizedFieldName = 'Reference URLs';
const initial = ['http://url-1'];
const customized = ['http://url-2'];
const upgrade = ['http://url-3'];
const resolvedValue = ['http://resolved'];
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,72 @@
/*
* 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 { mockRelatedIntegrations } from '../../test_utils/rule_upgrade_flyout';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "related_integrations" (query rule type) after preview in flyout', () => {
beforeAll(() => {
mockRelatedIntegrations([
{
package: 'packageResolved',
version: '5.0.0',
},
]);
});
const ruleType = 'query';
const fieldName = 'related_integrations';
const humanizedFieldName = 'Related Integrations';
const initial = [
{
package: 'packageA',
version: '^1.0.0',
},
];
const customized = [
{
package: 'packageB',
version: '^1.0.0',
},
];
const upgrade = [
{
package: 'packageC',
version: '^1.0.0',
},
];
const resolvedValue = [
{
package: 'packageResolved',
version: '^9.0.0',
},
];
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,65 @@
/*
* 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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "required_fields" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'required_fields';
const humanizedFieldName = 'Required fields';
const initial = [
{
name: 'fieldA',
type: 'string',
ecs: false,
},
];
const customized = [
{
name: 'fieldB',
type: 'string',
ecs: false,
},
];
const upgrade = [
{
name: 'fieldC',
type: 'string',
ecs: false,
},
];
const resolvedValue = [
{
name: 'resolvedStringField',
type: 'string',
},
];
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "risk_score" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'risk_score';
const humanizedFieldName = 'Risk Score';
const initial = 10;
const customized = 20;
const upgrade = 30;
const resolvedValue = 50;
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,80 @@
/*
* 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 { mockAvailableDataViews } from '../../test_utils/rule_upgrade_flyout';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "risk_score_mapping" (query rule type) after preview in flyout', () => {
beforeAll(() => {
mockAvailableDataViews([], {
resolvedNumber: {
name: 'resolvedNumberField',
type: 'number',
searchable: true,
aggregatable: true,
},
});
});
const ruleType = 'query';
const fieldName = 'risk_score_mapping';
const humanizedFieldName = 'Risk score override';
const initial = [
{
field: 'fieldA',
operator: 'equals',
value: '10',
risk_score: 10,
},
];
const customized = [
{
field: 'fieldB',
operator: 'equals',
value: '30',
risk_score: 30,
},
];
const upgrade = [
{
field: 'fieldC',
operator: 'equals',
value: '50',
risk_score: 50,
},
];
const resolvedValue = [
{
field: 'resolvedNumberField',
operator: 'equals',
},
];
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { mockAvailableDataViews } from '../../test_utils/rule_upgrade_flyout';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "rule_name_override" (query rule type) after preview in flyout', () => {
beforeAll(() => {
mockAvailableDataViews([], {
resolvedString: {
name: 'resolvedStringField',
type: 'string',
searchable: true,
aggregatable: true,
},
});
});
const ruleType = 'query';
const fieldName = 'rule_name_override';
const humanizedFieldName = 'Rule name override';
const initial = { field_name: 'fieldA' };
const customized = { field_name: 'fieldB' };
const upgrade = { field_name: 'fieldC' };
const resolvedValue = { field_name: 'resolvedStringField' };
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,58 @@
/*
* 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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "rule_schedule" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'rule_schedule';
const humanizedFieldName = 'Rule Schedule';
const initial = {
interval: '5m',
from: 'now-10m',
to: 'now',
};
const customized = {
interval: '10m',
from: 'now-1h',
to: 'now',
};
const upgrade = {
interval: '15m',
from: 'now-20m',
to: 'now',
};
const resolvedValue = {
interval: '1h',
from: 'now-2h',
to: 'now',
};
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "setup" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'setup';
const humanizedFieldName = 'Setup';
const initial = 'Initial setup';
const customized = 'Custom setup';
const upgrade = 'Updated setup';
const resolvedValue = 'resolved setup';
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "severity" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'severity';
const humanizedFieldName = 'Severity';
const initial = 'low';
const customized = 'medium';
const upgrade = 'high';
const resolvedValue = 'critical';
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,82 @@
/*
* 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 { mockAvailableDataViews } from '../../test_utils/rule_upgrade_flyout';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "severity_mapping" (query rule type) after preview in flyout', () => {
beforeAll(() => {
mockAvailableDataViews([], {
resolvedString: {
name: 'resolvedStringField',
type: 'string',
searchable: true,
aggregatable: true,
},
});
});
const ruleType = 'query';
const fieldName = 'severity_mapping';
const humanizedFieldName = 'Severity override';
const initial = [
{
field: 'fieldA',
operator: 'equals',
severity: 'low',
value: '10',
},
];
const customized = [
{
field: 'fieldB',
operator: 'equals',
severity: 'medium',
value: '30',
},
];
const upgrade = [
{
field: 'fieldC',
operator: 'equals',
severity: 'high',
value: '50',
},
];
const resolvedValue = [
{
field: 'resolvedStringField',
value: '70',
operator: 'equals',
severity: 'critical',
},
];
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,78 @@
/*
* 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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "threat" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'threat';
const humanizedFieldName = 'MITRE ATT&CK\u2122';
const initial = [
{
framework: 'MITRE ATT&CK',
tactic: {
name: 'tacticA',
id: 'tacticA',
reference: 'reference',
},
},
];
const customized = [
{
framework: 'MITRE ATT&CK',
tactic: {
name: 'tacticB',
id: 'tacticB',
reference: 'reference',
},
},
];
const upgrade = [
{
framework: 'MITRE ATT&CK',
tactic: {
name: 'tacticC',
id: 'tacticC',
reference: 'reference',
},
},
];
const resolvedValue = [
{
framework: 'MITRE ATT&CK',
tactic: {
name: 'Credential Access',
id: 'TA0006',
reference: 'https://attack.mitre.org/tactics/TA0006/',
},
},
];
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,52 @@
/*
* 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 { mockTimelines } from '../../test_utils/rule_upgrade_flyout';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "timeline_template" (query rule type) after preview in flyout', () => {
beforeAll(() => {
mockTimelines([
{
id: 'resolved',
title: 'timelineResolved',
},
]);
});
const ruleType = 'query';
const fieldName = 'timeline_template';
const humanizedFieldName = 'Timeline template';
const initial = { timeline_id: 'A', timeline_title: 'timelineA' };
const customized = { timeline_id: 'B', timeline_title: 'timelineB' };
const upgrade = { timeline_id: 'C', timeline_title: 'timelineC' };
const resolvedValue = { timeline_id: 'resolved', timeline_title: 'timelineResolved' };
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { mockAvailableDataViews } from '../../test_utils/rule_upgrade_flyout';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "timestamp_override" (query rule type) after preview in flyout', () => {
beforeAll(() => {
mockAvailableDataViews([], {
resolvedDate: {
name: 'resolvedDateField',
type: 'date',
searchable: true,
aggregatable: true,
},
});
});
const ruleType = 'query';
const fieldName = 'timestamp_override';
const humanizedFieldName = 'Timestamp override';
const initial = { field_name: 'fieldA', fallback_disabled: false };
const customized = { field_name: 'fieldB', fallback_disabled: false };
const upgrade = { field_name: 'fieldC', fallback_disabled: false };
const resolvedValue = { field_name: 'resolvedDateField', fallback_disabled: false };
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "anomaly_threshold" (machine_learning rule type) after preview in flyout', () => {
const ruleType = 'machine_learning';
const fieldName = 'anomaly_threshold';
const humanizedFieldName = 'Anomaly score threshold';
const initial = 10;
const customized = 20;
const upgrade = 30;
const resolvedValue = 40;
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,65 @@
/*
* 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 { of } from 'rxjs';
import { KibanaServices } from '../../../../../../../../common/lib/kibana';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "eql_query" (eql rule type) after preview in flyout', () => {
beforeAll(() => {
// Mock EQL validation response. It shouldn't contain "errors" field for a valid EQL query.
(KibanaServices.get().data.search.search as jest.Mock).mockReturnValue(of({}));
});
const ruleType = 'eql';
const fieldName = 'eql_query';
const humanizedFieldName = 'EQL query';
const initial = {
query: 'any where true',
language: 'eql',
filters: [],
};
const customized = {
query: 'host where host.name == "something"',
language: 'eql',
filters: [],
};
const upgrade = {
query: 'process where process.name == "regsvr32.exe"',
language: 'eql',
filters: [],
};
const resolvedValue = {
query: 'process where event.name == "resolved"',
language: 'eql',
filters: [],
};
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "history_window_start" (new_terms rule type) after preview in flyout', () => {
const ruleType = 'new_terms';
const fieldName = 'history_window_start';
const humanizedFieldName = 'History Window Size';
const initial = 'now-1h';
const customized = 'now-2h';
const upgrade = 'now-3h';
const resolvedValue = 'now-5h';
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,17 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test/jest_integration',
rootDir: '../../../../../../../../../../../../../..',
roots: [
'<rootDir>/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management',
],
testMatch: ['**/type_specific_fields/*.test.[jt]s?(x)'],
openHandlesTimeout: 0,
forceExit: true,
};

View file

@ -0,0 +1,63 @@
/*
* 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 { KqlQueryType } from '../../../../../../../../../common/api/detection_engine';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "kql_query" (query rule type) after preview in flyout', () => {
const ruleType = 'query';
const fieldName = 'kql_query';
const humanizedFieldName = 'KQL query';
const initial = {
query: '*:*',
language: 'kuery',
type: KqlQueryType.inline_query,
filters: [],
};
const customized = {
query: '*:*',
language: 'kuery',
type: KqlQueryType.inline_query,
filters: [],
};
const upgrade = {
query: 'process.name:*.sys',
language: 'kuery',
type: KqlQueryType.inline_query,
filters: [],
};
const resolvedValue = {
query: '*:resolved',
language: 'kuery',
type: KqlQueryType.inline_query,
filters: [],
};
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,168 @@
/*
* 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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
import { mockKibanaFetchResponse } from '../../test_utils/rule_upgrade_flyout';
describe('Upgrade diffable rule "machine_learning_job_id" (machine_learning rule type) after preview in flyout', () => {
beforeAll(() => {
mockKibanaFetchResponse('/internal/ml/ml_capabilities', {
capabilities: {
isADEnabled: true,
isDFAEnabled: true,
isNLPEnabled: true,
canCreateJob: true,
canDeleteJob: true,
canOpenJob: true,
canCloseJob: true,
canResetJob: true,
canUpdateJob: true,
canForecastJob: true,
canDeleteForecast: true,
canCreateDatafeed: true,
canDeleteDatafeed: true,
canStartStopDatafeed: true,
canUpdateDatafeed: true,
canPreviewDatafeed: true,
canGetFilters: true,
canCreateCalendar: true,
canDeleteCalendar: true,
canCreateFilter: true,
canDeleteFilter: true,
canCreateDataFrameAnalytics: true,
canDeleteDataFrameAnalytics: true,
canStartStopDataFrameAnalytics: true,
canCreateMlAlerts: true,
canUseMlAlerts: true,
canViewMlNodes: true,
canCreateTrainedModels: true,
canDeleteTrainedModels: true,
canStartStopTrainedModels: true,
canCreateInferenceEndpoint: true,
canGetJobs: true,
canGetDatafeeds: true,
canGetCalendars: true,
canFindFileStructure: true,
canGetDataFrameAnalytics: true,
canGetAnnotations: true,
canCreateAnnotation: true,
canDeleteAnnotation: true,
canGetTrainedModels: true,
canTestTrainedModels: true,
canGetFieldInfo: true,
canGetMlInfo: true,
canUseAiops: true,
},
upgradeInProgress: false,
isPlatinumOrTrialLicense: true,
mlFeatureEnabledInSpace: true,
});
mockKibanaFetchResponse('/internal/ml/jobs/jobs_summary', [
{
id: 'jobResolved',
description: 'jobResolved',
groups: [],
jobState: 'opened',
datafeedIndices: [],
hasDatafeed: true,
datafeedId: 'jobResolved',
datafeedState: '',
isSingleMetricViewerJob: true,
awaitingNodeAssignment: false,
jobTags: {},
bucketSpanSeconds: 0,
},
]);
mockKibanaFetchResponse('/internal/ml/modules/get_module/', [
{
id: 'security_network',
title: 'test-module',
description: 'test-module',
type: 'test-module',
logoFile: 'test-module',
defaultIndexPattern: 'test-module',
query: {},
jobs: [
{
id: 'jobResolved',
config: {
groups: [],
description: '',
analysis_config: {
bucket_span: '1m',
detectors: [],
influencers: [],
},
analysis_limits: {
model_memory_limit: '1mb',
},
data_description: {
time_field: '@timestamp',
},
custom_settings: {
created_by: 'test',
custom_urls: [],
},
job_type: 'test',
},
},
],
datafeeds: [],
kibana: {},
},
]);
mockKibanaFetchResponse(
'/internal/ml/modules/recognize/apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*',
[
{
id: 'test-module',
title: 'test-module',
query: {},
description: 'test-module',
logo: {
icon: 'test-module',
},
},
]
);
});
const ruleType = 'machine_learning';
const fieldName = 'machine_learning_job_id';
const humanizedFieldName = 'Machine Learning job';
const initial = ['jobA'];
const customized = ['jobB'];
const upgrade = ['jobC'];
const resolvedValue = ['jobResolved'];
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { mockAvailableDataViews } from '../../test_utils/rule_upgrade_flyout';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "new_terms_fields" (new_terms rule type) after preview in flyout', () => {
beforeAll(() => {
mockAvailableDataViews([], {
resolved: {
name: 'resolved',
type: 'string',
searchable: true,
aggregatable: true,
},
});
});
const ruleType = 'new_terms';
const fieldName = 'new_terms_fields';
const humanizedFieldName = 'New Terms Fields';
const initial = ['fieldA'];
const customized = ['fieldB'];
const upgrade = ['fieldA', 'fieldC'];
const resolvedValue = ['resolved'];
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "threat_index" (threat_match rule type) after preview in flyout', () => {
const ruleType = 'threat_match';
const fieldName = 'threat_index';
const humanizedFieldName = 'Indicator index patterns';
const initial = ['indexA'];
const customized = ['indexB'];
const upgrade = ['indexC'];
const resolvedValue = ['resolved'];
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "threat_indicator_path" (threat_match rule type) after preview in flyout', () => {
const ruleType = 'threat_match';
const fieldName = 'threat_indicator_path';
const humanizedFieldName = 'Indicator prefix override';
const initial = 'fieldA';
const customized = 'fieldB';
const upgrade = 'fieldC';
const resolvedValue = 'resolvedStringField';
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,63 @@
/*
* 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 { KqlQueryType } from '../../../../../../../../../common/api/detection_engine';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "threat_query" (threat_match rule type) after preview in flyout', () => {
const ruleType = 'threat_match';
const fieldName = 'threat_query';
const humanizedFieldName = 'Indicator index query';
const initial = {
type: KqlQueryType.inline_query,
query: 'process.name:*.exe',
language: 'kuery',
filters: [],
};
const customized = {
type: KqlQueryType.inline_query,
query: 'process.name:*.sys',
language: 'kuery',
filters: [],
};
const upgrade = {
type: KqlQueryType.inline_query,
query: 'process.name:*.com',
language: 'kuery',
filters: [],
};
const resolvedValue = {
type: KqlQueryType.inline_query,
query: 'process.name:*.sys',
language: 'kuery',
filters: [],
};
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -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 { mockAvailableDataViews } from '../../test_utils/rule_upgrade_flyout';
import { assertRuleUpgradePreview } from '../../test_utils/assert_rule_upgrade_preview';
import { assertRuleUpgradeAfterReview } from '../../test_utils/assert_rule_upgrade_after_review';
describe('Upgrade diffable rule "threshold" (threshold rule type) after preview in flyout', () => {
beforeAll(() => {
mockAvailableDataViews([], {
resolved: {
name: 'resolved',
type: 'string',
searchable: true,
aggregatable: true,
},
});
});
const ruleType = 'threshold';
const fieldName = 'threshold';
const humanizedFieldName = 'Threshold';
const initial = { value: 10, field: ['fieldA'] };
const customized = { value: 20, field: ['fieldB'] };
const upgrade = { value: 30, field: ['fieldC'] };
const resolvedValue = { value: 50, field: ['resolved'] };
assertRuleUpgradePreview({
ruleType,
fieldName,
humanizedFieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
assertRuleUpgradeAfterReview({
ruleType,
fieldName,
fieldVersions: {
initial,
customized,
upgrade,
resolvedValue,
},
});
});

View file

@ -0,0 +1,26 @@
/*
* 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 initialState = {
loading: false,
isSignalIndexExists: true,
isAuthenticated: true,
hasEncryptionKey: true,
canUserCRUD: true,
canUserREAD: true,
hasIndexManage: true,
hasIndexMaintenance: true,
hasIndexWrite: true,
hasIndexRead: true,
hasIndexUpdateDelete: true,
signalIndexName: true,
signalIndexMappingOutdated: true,
};
export const useUserData = jest.fn().mockReturnValue([initialState]);
export const useUserInfo = jest.fn().mockReturnValue(initialState);

View file

@ -19,6 +19,7 @@ import {
INSTALL_PREBUILT_RULE_PREVIEW,
UPDATE_PREBUILT_RULE_PREVIEW,
UPDATE_PREBUILT_RULE_BUTTON,
FIELD_UPGRADE_WRAPPER,
PER_FIELD_DIFF_WRAPPER,
PER_FIELD_DIFF_DEFINITION_SECTION,
} from '../../../../screens/alerts_detection_rules';
@ -1169,14 +1170,15 @@ describe(
openRuleUpdatePreview(OUTDATED_RULE_1['security-rule'].name);
assertSelectedPreviewTab(PREVIEW_TABS.UPDATES); // Should be open by default
cy.get(PER_FIELD_DIFF_WRAPPER).should('have.length', 1);
cy.get(PER_FIELD_DIFF_WRAPPER).last().contains('Name').should('be.visible');
const nameFieldUpgradeWrapper = FIELD_UPGRADE_WRAPPER('name');
cy.get(nameFieldUpgradeWrapper).should('have.length', 1);
cy.get(nameFieldUpgradeWrapper).last().contains('Name').should('be.visible');
// expand Name field section
cy.get(PER_FIELD_DIFF_WRAPPER).last().contains('Name').click();
cy.get(nameFieldUpgradeWrapper).last().contains('Name').click();
cy.get(PER_FIELD_DIFF_WRAPPER).last().contains('Outdated rule 1').should('be.visible');
cy.get(PER_FIELD_DIFF_WRAPPER).last().contains('Updated rule 1').should('be.visible');
cy.get(nameFieldUpgradeWrapper).last().contains('Outdated rule 1').should('be.visible');
cy.get(nameFieldUpgradeWrapper).last().contains('Updated rule 1').should('be.visible');
});
it('User can see changes when updated rule is a different rule type', () => {

View file

@ -359,6 +359,8 @@ export const ESQL_QUERY_TITLE = '[data-test-subj="esqlQueryPropertyTitle"]';
export const ESQL_QUERY_VALUE = '[data-test-subj="esqlQueryPropertyValue"]';
export const PER_FIELD_DIFF_WRAPPER = '[data-test-subj="ruleUpgradePerFieldDiffWrapper"]';
export const FIELD_UPGRADE_WRAPPER = (fieldName: string) =>
`[data-test-subj="${fieldName}-upgradeWrapper"]`;
export const PER_FIELD_DIFF_DEFINITION_SECTION = '[data-test-subj="perFieldDiffDefinitionSection"]';
export const MODIFIED_RULE_BADGE = '[data-test-subj="upgradeRulesTableModifiedColumnBadge"]';