import { render, waitFor } from '@testing-library/react'; import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event'; import React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector'; import { dateTime } from '@grafana/data'; import { locationService, setDataSourceSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { AlertState, MatcherOperator } from 'app/plugins/datasource/alertmanager/types'; import { configureStore } from 'app/store/configureStore'; import { AccessControlAction } from 'app/types'; import Silences from './Silences'; import { fetchSilences, fetchAlerts, createOrUpdateSilence } from './api/alertmanager'; import { mockAlertmanagerAlert, mockDataSource, MockDataSourceSrv, mockSilence } from './mocks'; import { parseMatchers } from './utils/alertmanager'; import { DataSourceType } from './utils/datasource'; jest.mock('./api/alertmanager'); jest.mock('app/core/services/context_srv'); const TEST_TIMEOUT = 60000; const mocks = { api: { fetchSilences: jest.mocked(fetchSilences), fetchAlerts: jest.mocked(fetchAlerts), createOrUpdateSilence: jest.mocked(createOrUpdateSilence), }, contextSrv: jest.mocked(contextSrv), }; const renderSilences = (location = '/alerting/silences/') => { const store = configureStore(); locationService.push(location); return render( ); }; const dataSources = { am: mockDataSource({ name: 'Alertmanager', type: DataSourceType.Alertmanager, }), }; const ui = { silencesTable: byTestId('dynamic-table'), silenceRow: byTestId('row'), silencedAlertCell: byTestId('alerts'), addSilenceButton: byRole('button', { name: /new silence/i }), queryBar: byPlaceholderText('Search'), editor: { timeRange: byLabelText('Timepicker', { exact: false }), durationField: byLabelText('Duration'), durationInput: byRole('textbox', { name: /duration/i }), matchersField: byTestId('matcher'), matcherName: byPlaceholderText('label'), matcherValue: byPlaceholderText('value'), comment: byPlaceholderText('Details about the silence'), matcherOperatorSelect: byLabelText('operator'), matcherOperator: (operator: MatcherOperator) => byText(operator, { exact: true }), addMatcherButton: byRole('button', { name: 'Add matcher' }), submit: byText('Submit'), }, }; const resetMocks = () => { jest.resetAllMocks(); mocks.api.fetchSilences.mockImplementation(() => { return Promise.resolve([ mockSilence({ id: '12345' }), mockSilence({ id: '67890', matchers: parseMatchers('foo!=bar'), comment: 'Catch all' }), ]); }); mocks.api.fetchAlerts.mockImplementation(() => { return Promise.resolve([ mockAlertmanagerAlert({ labels: { foo: 'bar' }, status: { state: AlertState.Suppressed, silencedBy: ['12345'], inhibitedBy: [] }, }), mockAlertmanagerAlert({ labels: { foo: 'buzz' }, status: { state: AlertState.Suppressed, silencedBy: ['67890'], inhibitedBy: [] }, }), ]); }); mocks.api.createOrUpdateSilence.mockResolvedValue(mockSilence()); mocks.contextSrv.evaluatePermission.mockImplementation(() => []); mocks.contextSrv.hasPermission.mockImplementation((action) => { const permissions = [ AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstanceCreate, AccessControlAction.AlertingInstanceUpdate, AccessControlAction.AlertingInstancesExternalRead, AccessControlAction.AlertingInstancesExternalWrite, ]; return permissions.includes(action as AccessControlAction); }); mocks.contextSrv.hasAccess.mockImplementation(() => true); }; describe('Silences', () => { beforeAll(resetMocks); afterEach(resetMocks); beforeEach(() => { setDataSourceSrv(new MockDataSourceSrv(dataSources)); }); it( 'loads and shows silences', async () => { renderSilences(); await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); expect(ui.silencesTable.query()).not.toBeNull(); const silences = ui.silenceRow.queryAll(); expect(silences).toHaveLength(2); expect(silences[0]).toHaveTextContent('foo=bar'); expect(silences[1]).toHaveTextContent('foo!=bar'); }, TEST_TIMEOUT ); it( 'shows the correct number of silenced alerts', async () => { mocks.api.fetchAlerts.mockImplementation(() => { return Promise.resolve([ mockAlertmanagerAlert({ labels: { foo: 'bar', buzz: 'bazz' }, status: { state: AlertState.Suppressed, silencedBy: ['12345'], inhibitedBy: [] }, }), mockAlertmanagerAlert({ labels: { foo: 'bar', buzz: 'bazz' }, status: { state: AlertState.Suppressed, silencedBy: ['12345'], inhibitedBy: [] }, }), ]); }); renderSilences(); await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); const silencedAlertRows = ui.silencedAlertCell.getAll(ui.silencesTable.get()); expect(silencedAlertRows).toHaveLength(2); expect(silencedAlertRows[0]).toHaveTextContent('2'); expect(silencedAlertRows[1]).toHaveTextContent('0'); }, TEST_TIMEOUT ); it( 'filters silences by matchers', async () => { renderSilences(); await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); const queryBar = ui.queryBar.get(); await userEvent.click(queryBar); await userEvent.paste('foo=bar'); await waitFor(() => expect(ui.silenceRow.getAll()).toHaveLength(1)); }, TEST_TIMEOUT ); it('shows creating a silence button for users with access', async () => { renderSilences(); await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); expect(ui.addSilenceButton.get()).toBeInTheDocument(); }); it('hides actions for creating a silence for users without access', async () => { mocks.contextSrv.hasAccess.mockImplementation((action) => { const permissions = [AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead]; return permissions.includes(action as AccessControlAction); }); renderSilences(); await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); expect(ui.addSilenceButton.query()).not.toBeInTheDocument(); }); }); describe('Silence edit', () => { const baseUrlPath = '/alerting/silence/new'; beforeAll(resetMocks); afterEach(resetMocks); beforeEach(() => { setDataSourceSrv(new MockDataSourceSrv(dataSources)); }); it( 'prefills the matchers field with matchers params', async () => { const matchersParams = ['foo=bar', 'bar=~ba.+', 'hello!=world', 'cluster!~us-central.*']; const matchersQueryString = matchersParams.map((matcher) => `matcher=${encodeURIComponent(matcher)}`).join('&'); renderSilences(`${baseUrlPath}?${matchersQueryString}`); await waitFor(() => expect(ui.editor.durationField.query()).not.toBeNull()); const matchers = ui.editor.matchersField.queryAll(); expect(matchers).toHaveLength(4); expect(ui.editor.matcherName.query(matchers[0])).toHaveValue('foo'); expect(ui.editor.matcherOperator(MatcherOperator.equal).query(matchers[0])).not.toBeNull(); expect(ui.editor.matcherValue.query(matchers[0])).toHaveValue('bar'); expect(ui.editor.matcherName.query(matchers[1])).toHaveValue('bar'); expect(ui.editor.matcherOperator(MatcherOperator.regex).query(matchers[1])).not.toBeNull(); expect(ui.editor.matcherValue.query(matchers[1])).toHaveValue('ba.+'); expect(ui.editor.matcherName.query(matchers[2])).toHaveValue('hello'); expect(ui.editor.matcherOperator(MatcherOperator.notEqual).query(matchers[2])).not.toBeNull(); expect(ui.editor.matcherValue.query(matchers[2])).toHaveValue('world'); expect(ui.editor.matcherName.query(matchers[3])).toHaveValue('cluster'); expect(ui.editor.matcherOperator(MatcherOperator.notRegex).query(matchers[3])).not.toBeNull(); expect(ui.editor.matcherValue.query(matchers[3])).toHaveValue('us-central.*'); }, TEST_TIMEOUT ); it( 'creates a new silence', async () => { renderSilences(baseUrlPath); await waitFor(() => expect(ui.editor.durationField.query()).not.toBeNull()); const start = new Date(); const end = new Date(start.getTime() + 24 * 60 * 60 * 1000); const now = dateTime().format('YYYY-MM-DD HH:mm'); const startDateString = dateTime(start).format('YYYY-MM-DD'); const endDateString = dateTime(end).format('YYYY-MM-DD'); await userEvent.clear(ui.editor.durationInput.get()); await userEvent.type(ui.editor.durationInput.get(), '1d'); await waitFor(() => expect(ui.editor.durationInput.query()).toHaveValue('1d')); await waitFor(() => expect(ui.editor.timeRange.get()).toHaveTextContent(startDateString)); await waitFor(() => expect(ui.editor.timeRange.get()).toHaveTextContent(endDateString)); await userEvent.type(ui.editor.matcherName.get(), 'foo'); await userEvent.type(ui.editor.matcherOperatorSelect.get(), '='); await userEvent.tab(); await userEvent.type(ui.editor.matcherValue.get(), 'bar'); // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed await userEvent.click(ui.editor.addMatcherButton.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); await userEvent.type(ui.editor.matcherName.getAll()[1], 'bar'); await userEvent.type(ui.editor.matcherOperatorSelect.getAll()[1], '!='); await userEvent.tab(); await userEvent.type(ui.editor.matcherValue.getAll()[1], 'buzz'); // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed await userEvent.click(ui.editor.addMatcherButton.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); await userEvent.type(ui.editor.matcherName.getAll()[2], 'region'); await userEvent.type(ui.editor.matcherOperatorSelect.getAll()[2], '=~'); await userEvent.tab(); await userEvent.type(ui.editor.matcherValue.getAll()[2], 'us-west-.*'); // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed await userEvent.click(ui.editor.addMatcherButton.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never }); await userEvent.type(ui.editor.matcherName.getAll()[3], 'env'); await userEvent.type(ui.editor.matcherOperatorSelect.getAll()[3], '!~'); await userEvent.tab(); await userEvent.type(ui.editor.matcherValue.getAll()[3], 'dev|staging'); await userEvent.click(ui.editor.submit.get()); await waitFor(() => expect(mocks.api.createOrUpdateSilence).toHaveBeenCalledWith( 'grafana', expect.objectContaining({ comment: `created ${now}`, matchers: [ { isEqual: true, isRegex: false, name: 'foo', value: 'bar' }, { isEqual: false, isRegex: false, name: 'bar', value: 'buzz' }, { isEqual: true, isRegex: true, name: 'region', value: 'us-west-.*' }, { isEqual: false, isRegex: true, name: 'env', value: 'dev|staging' }, ], }) ) ); }, TEST_TIMEOUT ); });