mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 10:03:33 -06:00
* Add and configure eslint-plugin-import * Fix the lint:ts npm command * Autofix + prettier all the files * Manually fix remaining files * Move jquery code in jest-setup to external file to safely reorder imports * Resolve issue caused by circular dependencies within Prometheus * Update .betterer.results * Fix missing // @ts-ignore * ignore iconBundle.ts * Fix missing // @ts-ignore
313 lines
12 KiB
TypeScript
313 lines
12 KiB
TypeScript
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
|
|
);
|
|
});
|