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(); }); });