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(
);
// 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(
);
// 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(
);
// 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(
);
// 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(
);
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(
);
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(
);
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(
{element}} />
,
{
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();
expect(screen.queryByText(/wait/i)).not.toBeInTheDocument();
});
it('should render only repeat interval', () => {
render();
expect(screen.getByText(/repeated every/i)).toBeInTheDocument();
expect(screen.getByText('5h')).toBeInTheDocument();
});
it('should render all options', () => {
render();
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();
});
});