Skip to content

Commit

Permalink
Feat/6389 allow adding notifications to illuminate events (#17999)
Browse files Browse the repository at this point in the history
* feat(6389): Allow add notification to Illuminate Events

* feat(6389): Remove unused state variable

* feat(6389): Added first tests and mocks

* feat(6389): Finished tests for Event Definitions forms

* feat(6389): Add changelog

* feat(6389): Updates from PR reaview comments
  • Loading branch information
zeeklop authored Jan 22, 2024
1 parent 799cea1 commit 2537f09
Show file tree
Hide file tree
Showing 10 changed files with 352 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ type Props = {
},
currentUser: User,
onChange: (name: string, newConfig: EventDefinition['config']) => void,
canEdit: boolean,
}

const EventConditionForm = ({ action, entityTypes, eventDefinition, validation, currentUser, onChange }: Props) => {
const EventConditionForm = ({ action, entityTypes, eventDefinition, validation, currentUser, onChange, canEdit }: Props) => {
const { pathname } = useLocation();
const sendTelemetry = useSendTelemetry();

Expand Down Expand Up @@ -111,6 +112,7 @@ const EventConditionForm = ({ action, entityTypes, eventDefinition, validation,

const eventDefinitionType = getConditionPlugin(eventDefinition.config.type);
const isSystemEventDefinition = eventDefinition.config.type === SYSTEM_EVENT_DEFINITION_TYPE;
const canEditCondition = canEdit && !isSystemEventDefinition;

const eventDefinitionTypeComponent = eventDefinitionType?.formComponent
? React.createElement<React.ComponentProps<any>>(eventDefinitionType.formComponent, {
Expand All @@ -129,9 +131,9 @@ const EventConditionForm = ({ action, entityTypes, eventDefinition, validation,
<Col md={7} lg={6}>
<h2 className={commonStyles.title}>Event Condition</h2>

{isSystemEventDefinition ? (
{!canEditCondition ? (
<p>
The conditions of system notification event definitions cannot be edited.
The conditions of this event definition type cannot be edited.
</p>
) : (
<>
Expand All @@ -156,7 +158,7 @@ const EventConditionForm = ({ action, entityTypes, eventDefinition, validation,
)}
</Col>

{!isSystemEventDefinition && !disabledSelect() && (
{canEditCondition && !disabledSelect() && (
<>
<Col md={5} lg={5} lgOffset={1}>
<HelpPanel className={styles.conditionTypesInfo}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type Props = {
onChange: (key: string, value: unknown) => void,
onCancel: () => void,
onSubmit: () => void
canEdit: boolean,
}

const EventDefinitionForm = ({
Expand All @@ -92,6 +93,7 @@ const EventDefinitionForm = ({
onChange,
onCancel,
onSubmit,
canEdit,
}: Props) => {
const { step } = useQuery();
const [activeStep, setActiveStep] = useState(step as string || STEP_KEYS[0]);
Expand Down Expand Up @@ -135,17 +137,17 @@ const EventDefinitionForm = ({
{
key: STEP_KEYS[0],
title: 'Event Details',
component: <EventDetailsForm {...defaultStepProps} />,
component: <EventDetailsForm {...defaultStepProps} canEdit={canEdit} />,
},
{
key: STEP_KEYS[1],
title: defaultTo(eventDefinitionType.displayName, 'Condition'),
component: <EventConditionForm {...defaultStepProps} />,
component: <EventConditionForm {...defaultStepProps} canEdit={canEdit} />,
},
{
key: STEP_KEYS[2],
title: 'Fields',
component: <FieldsForm {...defaultStepProps} />,
component: <FieldsForm {...defaultStepProps} canEdit={canEdit} />,
},
{
key: STEP_KEYS[3],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/

import * as React from 'react';
import { render, screen } from 'wrappedTestingLibrary';
import userEvent from '@testing-library/user-event';
import { defaultUser as mockDefaultUser } from 'defaultMockValues';

import useLocation from 'routing/useLocation';
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
import { asMock } from 'helpers/mocking';
import { simpleEventDefinition as mockEventDefinition } from 'fixtures/eventDefinition';
import useScopePermissions from 'hooks/useScopePermissions';
import useCurrentUser from 'hooks/useCurrentUser';
import useEventDefinitionConfigFromLocalStorage from 'components/event-definitions/hooks/useEventDefinitionConfigFromLocalStorage';

import EventDefinitionFormContainer from './EventDefinitionFormContainer';

type entityScope = {
is_mutable: boolean;
};

type getPermissionsByScopeReturnType = {
loadingScopePermissions: boolean;
scopePermissions: entityScope;
};

const mockAggregationEventDefinition = {
...mockEventDefinition,
config: {
...mockEventDefinition.config,
query: 'http_response_code:400',
},
};

const exampleEntityScopeMutable: getPermissionsByScopeReturnType = {
loadingScopePermissions: false,
scopePermissions: { is_mutable: true },
};

const exampleEntityScopeImmutable: getPermissionsByScopeReturnType = {
loadingScopePermissions: false,
scopePermissions: { is_mutable: false },
};

jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');

return {
...original,
useNavigate: () => jest.fn(),
};
});

jest.mock('stores/connect', () => ({
__esModule: true,
useStore: jest.fn((store) => store.getInitialState()),
default: jest.fn((
Component: React.ComponentType<React.ComponentProps<any>>,
stores: { [key: string]: any },
_mapProps: (args: { [key: string]: any }) => any,
) => {
const storeProps = Object.entries(stores).reduce((acc, [key, store]) => ({ ...acc, [key]: store.getInitialState() }), {});
const componentProps = { ...storeProps, eventDefinition: { ...mockEventDefinition, config: { ...mockEventDefinition.config, query: 'http_response_code:400' } } };

const ConnectStoreWrapper = () => (<Component {...componentProps} />);

return ConnectStoreWrapper;
}),
}));

jest.mock('stores/event-definitions/AvailableEventDefinitionTypesStore', () => ({
AvailableEventDefinitionTypesStore: {
getInitialState: () => ({
aggregation_functions: ['avg', 'card', 'count', 'max', 'min', 'sum', 'stddev', 'sumofsquares', 'variance', 'percentage', 'percentile', 'latest'],
field_provider_types: ['template-v1', 'lookup-v1'],
processor_types: ['aggregation-v1', 'system-notifications-v1', 'correlation-v1', 'anomaly-v1', 'sigma-v1'],
storage_handler_types: ['persist-to-streams-v1'],
}),
},
}));

const mockEventNotifications = [{
id: 'mock-notification-id',
title: 'mock-notification-title',
description: 'mock-notification-description',
config: {
body_template: '',
email_recipients: ['[email protected]'],
html_body_template: '',
lookup_recipient_emails: false,
lookup_reply_to_email: false,
lookup_sender_email: false,
recipients_lut_key: null,
recipients_lut_name: null,
reply_to: '',
reply_to_lut_key: null,
reply_to_lut_name: null,
sender: '[email protected]',
sender_lut_key: null,
sender_lut_name: null,
subject: 'Mock test email notification subject',
time_zone: 'UTC',
type: 'email-notification-v1',
user_recipients: [],
},
}];

jest.mock('stores/event-notifications/EventNotificationsStore', () => ({
EventNotificationsActions: { listAll: jest.fn() },
EventNotificationsStore: {
listen: () => jest.fn(),
getInitialState: () => ({
all: mockEventNotifications,
allLegacyTypes: [],
notifications: mockEventNotifications,
query: '',
pagination: {
count: 1,
page: 1,
pageSize: 10,
total: 1,
grandTotal: 1,
},
}),
},
}));

jest.mock('stores/configurations/ConfigurationsStore', () => ({
ConfigurationsActions: {
listEventsClusterConfig: jest.fn(() => Promise.resolve({
events_catchup_window: 3600000,
events_notification_default_backlog: 50,
events_notification_retry_period: 300000,
events_notification_tcp_keepalive: false,
events_search_timeout: 60000,
})),
},
}));

jest.mock('stores/users/CurrentUserStore', () => ({
__esModule: true,
CurrentUserStore: {
listen: () => jest.fn(),
getInitialState: () => ({ currentUser: mockDefaultUser.toJSON() }),
},
}));

jest.mock('../event-definition-types/withStreams', () => ({
__esModule: true,
default: (Component: React.FC) => (props: any) => (
<Component {...props} streams={[{ id: 'stream-id', title: 'stream-title' }]} />
),
}));

jest.mock('logic/telemetry/withTelemetry', () => ({
__esModule: true,
default: (Component: React.FC) => (props: any) => (
<Component {...props}
streams={[{ id: 'stream-id', title: 'stream-title' }]}
sendTelemetry={() => {}}
onChange={() => {}}
currentUser={mockDefaultUser}
validation={{ errors: {} }} />
),
}));

jest.mock('components/event-definitions/hooks/useEventDefinitionConfigFromLocalStorage');
jest.mock('routing/useLocation');
jest.mock('logic/telemetry/useSendTelemetry');
jest.mock('hooks/useScopePermissions');
jest.mock('hooks/useCurrentUser');

describe('EventDefinitionFormContainer', () => {
beforeEach(() => {
asMock(useLocation).mockImplementation(() => ({ pathname: '/event-definitions', search: '', hash: '', state: null, key: 'mock-key' }));
asMock(useSendTelemetry).mockImplementation(() => jest.fn());
asMock(useCurrentUser).mockImplementation(() => mockDefaultUser);
asMock(useEventDefinitionConfigFromLocalStorage).mockImplementation(() => ({ hasLocalStorageConfig: false, configFromLocalStorage: undefined }));
asMock(useScopePermissions).mockImplementation(() => exampleEntityScopeMutable);
});

it('should render Event Details form enabled', async () => {
render(<EventDefinitionFormContainer action="edit" eventDefinition={mockAggregationEventDefinition} />);

const titles = await screen.findAllByText(/event details/i);
titles.forEach((title) => expect(title).toBeInTheDocument());

expect(screen.getByRole('textbox', { name: /title/i })).toBeEnabled();
expect(screen.getByRole('textbox', { name: /description/i })).toBeEnabled();
});

it('should render Event Details form disabled for immutable entities', async () => {
asMock(useScopePermissions).mockImplementation(() => exampleEntityScopeImmutable);
render(<EventDefinitionFormContainer action="edit" eventDefinition={mockAggregationEventDefinition} />);

const titles = await screen.findAllByText(/event details/i);
titles.forEach((title) => expect(title).toBeInTheDocument());

expect(screen.getByRole('textbox', { name: /title/i })).toHaveAttribute('readonly');
expect(screen.getByRole('textbox', { name: /description/i })).toHaveAttribute('readonly');
});

it('should render Filters & Aggregation form enabled', async () => {
render(<EventDefinitionFormContainer action="edit" eventDefinition={mockAggregationEventDefinition} />);

const tab = await screen.findByRole('button', { name: /filter & aggregation/i });
userEvent.click(tab);

expect(screen.getByRole('textbox', { name: /search query/i })).toBeEnabled();
});

it('Filters & Aggregation should not be accessible for immutable entities', async () => {
asMock(useScopePermissions).mockImplementation(() => exampleEntityScopeImmutable);
render(<EventDefinitionFormContainer action="edit" eventDefinition={mockAggregationEventDefinition} />);

const tab = await screen.findByRole('button', { name: /filter & aggregation/i });
userEvent.click(tab);

expect(screen.getByText(/cannot be edited/i)).toBeVisible();
});

it('should render Fields form enabled', async () => {
render(<EventDefinitionFormContainer action="edit" eventDefinition={mockAggregationEventDefinition} />);

const tab = await screen.findByRole('button', { name: /fields/i });
userEvent.click(tab);

expect(screen.getByRole('button', { name: /add custom field/i })).toBeEnabled();
});

it('Fields should not be accessible for immutable entities', async () => {
asMock(useScopePermissions).mockImplementation(() => exampleEntityScopeImmutable);
render(<EventDefinitionFormContainer action="edit" eventDefinition={mockAggregationEventDefinition} />);

const tab = await screen.findByRole('button', { name: /fields/i });
userEvent.click(tab);

expect(screen.getByText(/cannot be edited/i)).toBeVisible();
});

it('should render Notifications form enabled', async () => {
render(<EventDefinitionFormContainer action="edit" eventDefinition={mockAggregationEventDefinition} />);

const tab = await screen.findByRole('button', { name: /notifications/i });
userEvent.click(tab);

expect(screen.getByRole('button', { name: /add notification/i })).toBeEnabled();
});

it('Notifications should be accessible for immutable entities', async () => {
asMock(useScopePermissions).mockImplementation(() => exampleEntityScopeImmutable);
render(<EventDefinitionFormContainer action="edit" eventDefinition={mockAggregationEventDefinition} />);

const tab = await screen.findByRole('button', { name: /notification/i });
userEvent.click(tab);

expect(screen.getByRole('button', { name: /add notification/i })).toBeEnabled();
});

it('should be able to add notifications', async () => {
render(<EventDefinitionFormContainer action="edit" eventDefinition={mockAggregationEventDefinition} />);

const tab = await screen.findByRole('button', { name: /notification/i });
userEvent.click(tab);
const addNotificationButton = screen.getByRole('button', { name: /add notification/i });

expect(addNotificationButton).toBeEnabled();

userEvent.click(addNotificationButton);
userEvent.type(screen.getByText(/select notification/i), 'mock-notification-title{enter}');
userEvent.click(screen.getByRole('button', { name: /add notification/i }));

expect(screen.getByText(/mock-notification-title/i)).toBeVisible();
});

it('should be able to add notifications to immutable entities', async () => {
asMock(useScopePermissions).mockImplementation(() => exampleEntityScopeImmutable);
render(<EventDefinitionFormContainer action="edit" eventDefinition={mockAggregationEventDefinition} />);

const tab = await screen.findByRole('button', { name: /notification/i });
userEvent.click(tab);
const addNotificationButton = screen.getByRole('button', { name: /add notification/i });

expect(addNotificationButton).toBeEnabled();

userEvent.click(addNotificationButton);
userEvent.type(screen.getByText(/select notification/i), 'mock-notification-title{enter}');
userEvent.click(screen.getByRole('button', { name: /add notification/i }));

expect(screen.getByText(/mock-notification-title/i)).toBeVisible();
});
});
Loading

0 comments on commit 2537f09

Please sign in to comment.