123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- 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(
- <Provider store={store}>
- <Router history={locationService.getHistory()}>
- <Silences />
- </Router>
- </Provider>
- );
- };
- 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
- );
- });
|