514 lines
18 KiB
TypeScript
514 lines
18 KiB
TypeScript
import { renderHook, screen, within } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { first, noop } from 'lodash';
|
|
import { Route, Routes } from 'react-router-dom-v5-compat';
|
|
import { render } from 'test/test-utils';
|
|
|
|
import { config } from '@grafana/runtime';
|
|
import { contextSrv } from 'app/core/core';
|
|
import {
|
|
AlertmanagerGroup,
|
|
MatcherOperator,
|
|
ObjectMatcher,
|
|
RouteWithID,
|
|
} from 'app/plugins/datasource/alertmanager/types';
|
|
import { ReceiversState } from 'app/types/alerting';
|
|
|
|
import { useAlertmanagerAbilities } from '../../hooks/useAbilities';
|
|
import { mockReceiversState } from '../../mocks';
|
|
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
|
|
|
import {
|
|
AUTOGENERATED_ROOT_LABEL_NAME,
|
|
Policy,
|
|
TimingOptionsMeta,
|
|
isAutoGeneratedRootAndSimplifiedEnabled,
|
|
useCreateDropdownMenuActions,
|
|
} from './Policy';
|
|
|
|
jest.mock('../../hooks/useAbilities', () => ({
|
|
...jest.requireActual('../../hooks/useAbilities'),
|
|
useAlertmanagerAbilities: jest.fn(),
|
|
}));
|
|
|
|
const useAlertmanagerAbilitiesMock = jest.mocked(useAlertmanagerAbilities);
|
|
|
|
describe('Policy', () => {
|
|
beforeAll(() => {
|
|
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
|
useAlertmanagerAbilitiesMock.mockReturnValue([
|
|
[true, true],
|
|
[true, true],
|
|
[true, true],
|
|
]);
|
|
});
|
|
|
|
it('should render a policy tree', async () => {
|
|
const onEditPolicy = jest.fn();
|
|
const onAddPolicy = jest.fn();
|
|
const onDeletePolicy = jest.fn();
|
|
const onShowAlertInstances = jest.fn(
|
|
(alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[] | undefined) => {}
|
|
);
|
|
|
|
const routeTree = mockRoutes;
|
|
const user = userEvent.setup();
|
|
|
|
renderPolicy(
|
|
<Policy
|
|
receivers={[{ name: 'grafana-default-email' }]}
|
|
isDefaultPolicy
|
|
currentRoute={routeTree}
|
|
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
|
onEditPolicy={onEditPolicy}
|
|
onAddPolicy={onAddPolicy}
|
|
onDeletePolicy={onDeletePolicy}
|
|
onShowAlertInstances={onShowAlertInstances}
|
|
/>
|
|
);
|
|
|
|
// should have default policy
|
|
const defaultPolicy = screen.getByTestId('am-root-route-container');
|
|
expect(defaultPolicy).toBeInTheDocument();
|
|
expect(within(defaultPolicy).getByText('Default policy')).toBeVisible();
|
|
|
|
// click "more actions" and check if we can edit and delete
|
|
expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
|
|
await user.click(within(defaultPolicy).getByTestId('more-actions'));
|
|
|
|
// should be editable
|
|
const editDefaultPolicy = screen.getByRole('menuitem', { name: 'Edit' });
|
|
expect(editDefaultPolicy).toBeInTheDocument();
|
|
expect(editDefaultPolicy).toBeEnabled();
|
|
await user.click(editDefaultPolicy);
|
|
expect(onEditPolicy).toHaveBeenCalledWith(routeTree, true);
|
|
|
|
// should not be deletable
|
|
expect(screen.queryByRole('menuitem', { name: 'Delete' })).not.toBeInTheDocument();
|
|
|
|
// default policy should show the metadata
|
|
|
|
// no continue matching
|
|
expect(within(defaultPolicy).queryByTestId('continue-matching')).not.toBeInTheDocument();
|
|
|
|
// for matching instances
|
|
// expect(within(defaultPolicy).getByTestId('matching-instances')).toHaveTextContent('0instances');
|
|
|
|
// for contact point
|
|
expect(within(defaultPolicy).getByTestId('contact-point')).toHaveTextContent('grafana-default-email');
|
|
expect(within(defaultPolicy).getByRole('link', { name: 'grafana-default-email' })).toBeInTheDocument();
|
|
|
|
// for grouping
|
|
expect(within(defaultPolicy).getByTestId('grouping')).toHaveTextContent('grafana_folder, alertname');
|
|
|
|
// no timings
|
|
expect(within(defaultPolicy).queryByTestId('mute-timings')).not.toBeInTheDocument();
|
|
expect(within(defaultPolicy).queryByTestId('active-timings')).not.toBeInTheDocument();
|
|
|
|
// for timing options
|
|
expect(within(defaultPolicy).getByTestId('timing-options')).toHaveTextContent(
|
|
'Wait 30s to group instances · Wait 5m before sending updates · Repeated every 4h'
|
|
);
|
|
|
|
// should have custom policies
|
|
const customPolicies = screen.getAllByTestId('am-route-container');
|
|
expect(customPolicies).toHaveLength(3);
|
|
|
|
// all policies should be editable and deletable
|
|
for (const container of customPolicies) {
|
|
const policy = within(container);
|
|
|
|
// click "more actions" and check if we can delete
|
|
await user.click(policy.getByTestId('more-actions'));
|
|
expect(screen.queryByRole('menuitem', { name: 'Edit' })).toBeEnabled();
|
|
expect(screen.queryByRole('menuitem', { name: 'Delete' })).toBeEnabled();
|
|
|
|
await user.click(screen.getByRole('menuitem', { name: 'Delete' }));
|
|
expect(onDeletePolicy).toHaveBeenCalled();
|
|
}
|
|
|
|
// first custom policy should have the correct information
|
|
const firstPolicy = customPolicies[0];
|
|
expect(within(firstPolicy).getByTestId('label-matchers')).toHaveTextContent(/^team \= operations$/);
|
|
expect(within(firstPolicy).getByTestId('continue-matching')).toBeInTheDocument();
|
|
// expect(within(firstPolicy).getByTestId('matching-instances')).toHaveTextContent('0instances');
|
|
expect(within(firstPolicy).getByTestId('contact-point')).toHaveTextContent('provisioned-contact-point');
|
|
expect(within(firstPolicy).getByTestId('mute-timings')).toHaveTextContent('Muted when mt-1');
|
|
expect(within(firstPolicy).getByTestId('active-timings')).toHaveTextContent('Active when mt-2');
|
|
expect(within(firstPolicy).getByTestId('inherited-properties')).toHaveTextContent('Inherited4 properties');
|
|
|
|
// second custom policy should be correct
|
|
const secondPolicy = customPolicies[1];
|
|
expect(within(secondPolicy).getByTestId('label-matchers')).toHaveTextContent(/^region \= EMEA$/);
|
|
expect(within(secondPolicy).queryByTestId('continue-matching')).not.toBeInTheDocument();
|
|
expect(within(secondPolicy).queryByTestId('mute-timings')).not.toBeInTheDocument();
|
|
expect(within(secondPolicy).queryByTestId('active-timings')).not.toBeInTheDocument();
|
|
expect(within(secondPolicy).getByTestId('inherited-properties')).toHaveTextContent('Inherited5 properties');
|
|
|
|
// third custom policy should be correct
|
|
const thirdPolicy = customPolicies[2];
|
|
expect(within(thirdPolicy).getByTestId('label-matchers')).toHaveTextContent(
|
|
/^foo = barbar = bazbaz = quxasdf = asdftype = diskand 1 more$/
|
|
);
|
|
});
|
|
|
|
it('should show export option when export is allowed and supported returns true', async () => {
|
|
const onEditPolicy = jest.fn();
|
|
const onAddPolicy = jest.fn();
|
|
const onDeletePolicy = jest.fn();
|
|
const onShowAlertInstances = jest.fn(
|
|
(alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[] | undefined) => {}
|
|
);
|
|
|
|
const routeTree = mockRoutes;
|
|
const user = userEvent.setup();
|
|
|
|
renderPolicy(
|
|
<Policy
|
|
isDefaultPolicy
|
|
currentRoute={routeTree}
|
|
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
|
onEditPolicy={onEditPolicy}
|
|
onAddPolicy={onAddPolicy}
|
|
onDeletePolicy={onDeletePolicy}
|
|
onShowAlertInstances={onShowAlertInstances}
|
|
isAutoGenerated={false}
|
|
/>
|
|
);
|
|
// should have default policy
|
|
const defaultPolicy = screen.getByTestId('am-root-route-container');
|
|
// click "more actions"
|
|
expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
|
|
await user.click(within(defaultPolicy).getByTestId('more-actions'));
|
|
expect(screen.getByRole('menuitem', { name: 'Export' })).toBeInTheDocument();
|
|
});
|
|
|
|
it('should not show export option when is not supported', async () => {
|
|
const onEditPolicy = jest.fn();
|
|
const onAddPolicy = jest.fn();
|
|
const onDeletePolicy = jest.fn();
|
|
const onShowAlertInstances = jest.fn(
|
|
(alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[] | undefined) => {}
|
|
);
|
|
|
|
const routeTree = mockRoutes;
|
|
|
|
useAlertmanagerAbilitiesMock.mockReturnValue([
|
|
[true, true],
|
|
[true, true],
|
|
[false, true],
|
|
]);
|
|
|
|
const user = userEvent.setup();
|
|
|
|
renderPolicy(
|
|
<Policy
|
|
isDefaultPolicy
|
|
currentRoute={routeTree}
|
|
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
|
onEditPolicy={onEditPolicy}
|
|
onAddPolicy={onAddPolicy}
|
|
onDeletePolicy={onDeletePolicy}
|
|
onShowAlertInstances={onShowAlertInstances}
|
|
/>
|
|
);
|
|
// should have default policy
|
|
const defaultPolicy = screen.getByTestId('am-root-route-container');
|
|
// click "more actions"
|
|
expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
|
|
await user.click(within(defaultPolicy).getByTestId('more-actions'));
|
|
expect(screen.queryByRole('menuitem', { name: 'Export' })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should not show export option when is not allowed', async () => {
|
|
const onEditPolicy = jest.fn();
|
|
const onAddPolicy = jest.fn();
|
|
const onDeletePolicy = jest.fn();
|
|
const onShowAlertInstances = jest.fn(
|
|
(alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[] | undefined) => {}
|
|
);
|
|
|
|
const routeTree = mockRoutes;
|
|
|
|
useAlertmanagerAbilitiesMock.mockReturnValue([
|
|
[true, true],
|
|
[true, true],
|
|
[true, false],
|
|
]);
|
|
|
|
const user = userEvent.setup();
|
|
|
|
renderPolicy(
|
|
<Policy
|
|
isDefaultPolicy
|
|
currentRoute={routeTree}
|
|
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
|
onEditPolicy={onEditPolicy}
|
|
onAddPolicy={onAddPolicy}
|
|
onDeletePolicy={onDeletePolicy}
|
|
onShowAlertInstances={onShowAlertInstances}
|
|
/>
|
|
);
|
|
// should have default policy
|
|
const defaultPolicy = screen.getByTestId('am-root-route-container');
|
|
// click "more actions"
|
|
expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
|
|
await user.click(within(defaultPolicy).getByTestId('more-actions'));
|
|
expect(screen.queryByRole('menuitem', { name: 'Export' })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should not allow editing readOnly policy tree', () => {
|
|
const routeTree: RouteWithID = { id: '0', routes: [{ id: '1' }] };
|
|
|
|
renderPolicy(
|
|
<Policy
|
|
readOnly
|
|
currentRoute={routeTree}
|
|
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
|
onEditPolicy={noop}
|
|
onAddPolicy={noop}
|
|
onDeletePolicy={noop}
|
|
onShowAlertInstances={noop}
|
|
/>
|
|
);
|
|
|
|
expect(screen.queryByRole('button', { name: 'Edit' })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it.skip('should show matching instances', () => {
|
|
const routeTree: RouteWithID = {
|
|
id: '0',
|
|
routes: [{ id: '1', object_matchers: [['foo', eq, 'bar']] }],
|
|
};
|
|
|
|
renderPolicy(
|
|
<Policy
|
|
readOnly
|
|
isDefaultPolicy
|
|
currentRoute={routeTree}
|
|
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
|
onEditPolicy={noop}
|
|
onAddPolicy={noop}
|
|
onDeletePolicy={noop}
|
|
onShowAlertInstances={noop}
|
|
/>
|
|
);
|
|
|
|
const defaultPolicy = screen.getByTestId('am-root-route-container');
|
|
expect(within(defaultPolicy).getByTestId('matching-instances')).toHaveTextContent('1instance');
|
|
const customPolicy = screen.getByTestId('am-route-container');
|
|
expect(within(customPolicy).getByTestId('matching-instances')).toHaveTextContent('2instances');
|
|
});
|
|
|
|
it('should show warnings and errors', () => {
|
|
const routeTree: RouteWithID = {
|
|
id: '0', // this one should show an error
|
|
receiver: 'broken-receiver',
|
|
routes: [{ id: '1', object_matchers: [] }], // this one should show a warning
|
|
};
|
|
|
|
const receiversState: ReceiversState = mockReceiversState();
|
|
|
|
renderPolicy(
|
|
<Policy
|
|
readOnly
|
|
isDefaultPolicy
|
|
currentRoute={routeTree}
|
|
contactPointsState={receiversState}
|
|
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
|
onEditPolicy={noop}
|
|
onAddPolicy={noop}
|
|
onDeletePolicy={noop}
|
|
onShowAlertInstances={noop}
|
|
/>
|
|
);
|
|
|
|
const defaultPolicy = screen.getByTestId('am-root-route-container');
|
|
expect(within(defaultPolicy).queryByTestId('matches-all')).not.toBeInTheDocument();
|
|
expect(within(defaultPolicy).getByText('1 error')).toBeInTheDocument();
|
|
|
|
const customPolicy = screen.getByTestId('am-route-container');
|
|
expect(within(customPolicy).getByTestId('matches-all')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
// Doesn't matter which path the routes use, it just needs to match the initialEntries history entry to render the element
|
|
const renderPolicy = (element: JSX.Element) =>
|
|
render(
|
|
<Routes>
|
|
<Route path={'/'} element={<AlertmanagerProvider accessType="notification">{element}</AlertmanagerProvider>} />
|
|
</Routes>,
|
|
{
|
|
historyOptions: {
|
|
initialEntries: ['/'],
|
|
},
|
|
}
|
|
);
|
|
|
|
const eq = MatcherOperator.equal;
|
|
|
|
const mockRoutes: RouteWithID = {
|
|
id: '0',
|
|
receiver: 'grafana-default-email',
|
|
group_by: ['grafana_folder', 'alertname'],
|
|
routes: [
|
|
{
|
|
id: '1',
|
|
receiver: 'provisioned-contact-point',
|
|
object_matchers: [['team', eq, 'operations']],
|
|
mute_time_intervals: ['mt-1'],
|
|
active_time_intervals: ['mt-2'],
|
|
continue: true,
|
|
routes: [
|
|
{
|
|
id: '2',
|
|
object_matchers: [['region', eq, 'EMEA']],
|
|
},
|
|
{
|
|
id: '3',
|
|
receiver: 'grafana-default-email',
|
|
object_matchers: [
|
|
['foo', eq, 'bar'],
|
|
['bar', eq, 'baz'],
|
|
['baz', eq, 'qux'],
|
|
['asdf', eq, 'asdf'],
|
|
['type', eq, 'disk'],
|
|
['severity', eq, 'critical'],
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
group_wait: '30s',
|
|
group_interval: '5m',
|
|
repeat_interval: '4h',
|
|
};
|
|
|
|
describe('isAutoGeneratedRootAndSimplifiedEnabled', () => {
|
|
it('returns false when simplified routing is not enabled', () => {
|
|
const route: RouteWithID = {
|
|
id: '1',
|
|
object_matchers: [['label', MatcherOperator.equal, 'true']],
|
|
};
|
|
config.featureToggles.alertingSimplifiedRouting = false;
|
|
expect(isAutoGeneratedRootAndSimplifiedEnabled(route)).toBe(false);
|
|
});
|
|
|
|
it('returns false when object_matchers is not defined', () => {
|
|
const route: RouteWithID = {
|
|
id: '1',
|
|
};
|
|
config.featureToggles.alertingSimplifiedRouting = true;
|
|
expect(isAutoGeneratedRootAndSimplifiedEnabled(route)).toBe(false);
|
|
});
|
|
|
|
it('returns true when object_matchers contains AUTOGENERATED_ROOT_LABEL_NAME, and simplified routing is enabled', () => {
|
|
const route: RouteWithID = {
|
|
id: '1',
|
|
object_matchers: [[AUTOGENERATED_ROOT_LABEL_NAME, MatcherOperator.equal, 'true']],
|
|
};
|
|
config.featureToggles.alertingSimplifiedRouting = true;
|
|
expect(isAutoGeneratedRootAndSimplifiedEnabled(route)).toBe(true);
|
|
});
|
|
|
|
it('returns false when object_matchers does not contain AUTOGENERATED_ROOT_LABEL_NAME, and simplified routing is enabled', () => {
|
|
const route: RouteWithID = {
|
|
id: '1',
|
|
object_matchers: [['label', MatcherOperator.equal, 'true']],
|
|
};
|
|
config.featureToggles.alertingSimplifiedRouting = true;
|
|
expect(isAutoGeneratedRootAndSimplifiedEnabled(route)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('useCreateDropdownMenuActions', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
const openDetailModal = jest.fn();
|
|
const currentRoute: RouteWithID = { id: '0', routes: [{ id: '1' }] };
|
|
const toggleShowExportDrawer = jest.fn();
|
|
const onDeletePolicy = jest.fn();
|
|
const testCases = [
|
|
{
|
|
isAutoGenerated: false,
|
|
isDefaultPolicy: true,
|
|
provisioned: false,
|
|
expectedMenu: ['edit-policy', 'export-policy'],
|
|
},
|
|
{
|
|
isAutoGenerated: false,
|
|
isDefaultPolicy: true,
|
|
provisioned: true,
|
|
expectedMenu: ['edit-policy', 'export-policy'],
|
|
},
|
|
{
|
|
isAutoGenerated: false,
|
|
isDefaultPolicy: false,
|
|
provisioned: false,
|
|
expectedMenu: ['edit-policy', 'delete-policy'],
|
|
},
|
|
{
|
|
isAutoGenerated: false,
|
|
isDefaultPolicy: false,
|
|
provisioned: true,
|
|
expectedMenu: ['edit-policy', 'delete-policy'],
|
|
},
|
|
{ isAutoGenerated: true, isDefaultPolicy: true, provisioned: true, expectedMenu: ['edit-policy'] },
|
|
{ isAutoGenerated: true, isDefaultPolicy: false, provisioned: false, expectedMenu: ['edit-policy'] },
|
|
{ isAutoGenerated: true, isDefaultPolicy: true, provisioned: false, expectedMenu: ['edit-policy'] },
|
|
{ isAutoGenerated: true, isDefaultPolicy: false, provisioned: true, expectedMenu: ['edit-policy'] },
|
|
];
|
|
|
|
testCases.forEach(({ isAutoGenerated, isDefaultPolicy, provisioned, expectedMenu }) => {
|
|
it(`Having all the permissions returns ${expectedMenu.length} menu items for isAutoGenerated=${isAutoGenerated}, isDefaultPolicy=${isDefaultPolicy}, provisioned=${provisioned}`, () => {
|
|
useAlertmanagerAbilitiesMock.mockReturnValue([
|
|
[true, true],
|
|
[true, true],
|
|
[true, true],
|
|
]);
|
|
const { result } = renderHook(() =>
|
|
useCreateDropdownMenuActions(
|
|
isAutoGenerated,
|
|
isDefaultPolicy,
|
|
provisioned,
|
|
openDetailModal,
|
|
currentRoute,
|
|
toggleShowExportDrawer,
|
|
onDeletePolicy
|
|
)
|
|
);
|
|
|
|
const menuItemsKeys = result.current.map((item) => item.key ?? '');
|
|
expect(menuItemsKeys).toEqual(expectedMenu);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('TimingOptionsMeta', () => {
|
|
it('should render nothing without options', () => {
|
|
render(<TimingOptionsMeta timingOptions={{}} />);
|
|
expect(screen.queryByText(/wait/i)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should render only repeat interval', () => {
|
|
render(<TimingOptionsMeta timingOptions={{ repeat_interval: '5h' }} />);
|
|
expect(screen.getByText(/repeated every/i)).toBeInTheDocument();
|
|
expect(screen.getByText('5h')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render all options', () => {
|
|
render(<TimingOptionsMeta timingOptions={{ group_wait: '30s', group_interval: '5m', repeat_interval: '4h' }} />);
|
|
expect(
|
|
first(
|
|
screen.getAllByText(
|
|
(_, element) =>
|
|
element?.textContent === 'Wait 30s to group instances · Wait 5m before sending updates · Repeated every 4h',
|
|
{ collapseWhitespace: false, trim: false, exact: true }
|
|
)
|
|
)
|
|
).toBeInTheDocument();
|
|
});
|
|
});
|