The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/alerting/unified/MuteTimings.test.tsx

397 lines
12 KiB

import { InitialEntry } from 'history';
import { last } from 'lodash';
import { Route, Routes } from 'react-router-dom-v5-compat';
import { render, screen, userEvent, within } from 'test/test-utils';
import { byTestId } from 'testing-library-selector';
import { config } from '@grafana/runtime';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { captureRequests } from 'app/features/alerting/unified/mocks/server/events';
import { MOCK_DATASOURCE_EXTERNAL_VANILLA_ALERTMANAGER_UID } from 'app/features/alerting/unified/mocks/server/handlers/datasources';
import {
TIME_INTERVAL_NAME_FILE_PROVISIONED,
TIME_INTERVAL_NAME_HAPPY_PATH,
} from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import EditMuteTimingPage from './components/mute-timings/EditMuteTiming';
import NewMuteTimingPage from './components/mute-timings/NewMuteTiming';
import { grantUserPermissions, mockDataSource } from './mocks';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
const indexPageText = 'redirected routes page';
const Index = () => {
return <div>{indexPageText}</div>;
};
const renderMuteTimings = (location?: InitialEntry) => {
render(
<Routes>
<Route path={'/alerting/routes'} element={<Index />} />
<Route path={'/alerting/routes/new'} element={<NewMuteTimingPage />} />
<Route path={'/alerting/routes/edit'} element={<EditMuteTimingPage />} />
</Routes>,
{ historyOptions: location ? { initialEntries: [location] } : undefined }
);
};
const alertmanagerName = 'alertmanager';
const dataSources = {
am: mockDataSource({
name: alertmanagerName,
uid: MOCK_DATASOURCE_EXTERNAL_VANILLA_ALERTMANAGER_UID,
type: DataSourceType.Alertmanager,
}),
};
const ui = {
nameField: byTestId('mute-timing-name'),
startsAt: byTestId('mute-timing-starts-at'),
endsAt: byTestId('mute-timing-ends-at'),
weekdays: byTestId('mute-timing-weekdays'),
days: byTestId('mute-timing-days'),
months: byTestId('mute-timing-months'),
years: byTestId('mute-timing-years'),
};
const muteTimeInterval: MuteTimeInterval = {
name: 'default-mute',
time_intervals: [
{
times: [
{
start_time: '12:00',
end_time: '24:00',
},
],
days_of_month: ['15', '-1'],
months: ['august:december', 'march'],
},
],
};
const muteTimeInterval2: MuteTimeInterval = {
name: 'default-mute2',
time_intervals: [
{
times: [
{
start_time: '12:00',
end_time: '24:00',
},
],
days_of_month: ['15', '-1'],
months: ['august:december', 'march'],
},
],
};
/** Alertmanager config where time intervals are stored in `mute_time_intervals` property */
export const defaultConfig: AlertManagerCortexConfig = {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
receiver: 'default',
group_by: ['alertname'],
routes: [
{
matchers: ['env=prod', 'region!=EU'],
mute_time_intervals: [muteTimeInterval.name],
},
],
},
templates: [],
mute_time_intervals: [muteTimeInterval],
},
template_files: {},
};
/** Alertmanager config where time intervals are stored in `time_intervals` property */
const defaultConfigWithNewTimeIntervalsField: AlertManagerCortexConfig = {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
receiver: 'default',
group_by: ['alertname'],
routes: [
{
matchers: ['env=prod', 'region!=EU'],
mute_time_intervals: [muteTimeInterval.name],
},
],
},
templates: [],
time_intervals: [muteTimeInterval],
},
template_files: {},
};
/** Alertmanager config where time intervals are stored in both `time_intervals` and `mute_time_intervals` properties */
const defaultConfigWithBothTimeIntervalsField: AlertManagerCortexConfig = {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
receiver: 'default',
group_by: ['alertname'],
routes: [
{
matchers: ['env=prod', 'region!=EU'],
mute_time_intervals: [muteTimeInterval.name],
},
],
},
templates: [],
time_intervals: [muteTimeInterval],
mute_time_intervals: [muteTimeInterval2],
},
template_files: {},
};
const expectToHaveRedirectedToRoutesRoute = async () =>
expect(await screen.findByText(indexPageText)).toBeInTheDocument();
const fillOutForm = async ({
name,
startsAt,
endsAt,
days,
months,
years,
}: {
name?: string;
startsAt?: string;
endsAt?: string;
days?: string;
months?: string;
years?: string;
}) => {
const user = userEvent.setup();
name && (await user.type(ui.nameField.get(), name));
startsAt && (await user.type(ui.startsAt.get(), startsAt));
endsAt && (await user.type(ui.endsAt.get(), endsAt));
days && (await user.type(ui.days.get(), days));
months && (await user.type(ui.months.get(), months));
years && (await user.type(ui.years.get(), years));
};
const saveMuteTiming = async () => {
const user = userEvent.setup();
await user.click(await screen.findByText(/save mute timing/i));
};
setupMswServer();
const getAlertmanagerConfigUpdate = async (requests: Request[]): Promise<AlertManagerCortexConfig> => {
const alertmanagerUpdate = requests.find(
(r) => r.url.match('/alertmanager/(.*)/config/api/v1/alert') && r.method === 'POST'
);
return alertmanagerUpdate!.clone().json();
};
describe('Mute timings', () => {
beforeEach(() => {
setupDataSources(dataSources.am);
// FIXME: scope down
grantUserPermissions(Object.values(AccessControlAction));
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, defaultConfig);
// TODO: Add this at a higher level to ensure that no tests depend on others running first
// Without this, the selected alertmanager in a previous test can affect the next, meaning tests
// pass/fail depending on the order they are run/if they are focused
window.localStorage.clear();
});
it('creates a new mute timing, with mute_time_intervals in config', async () => {
const capture = captureRequests();
renderMuteTimings('/alerting/routes/new');
await screen.findByText(/add mute timing/i);
await fillOutForm({
name: 'maintenance period',
startsAt: '22:00',
endsAt: '24:00',
days: '-1',
months: 'january, july',
});
await saveMuteTiming();
await expectToHaveRedirectedToRoutesRoute();
const requests = await capture;
const alertmanagerUpdate = await getAlertmanagerConfigUpdate(requests);
const lastAdded = last(alertmanagerUpdate.alertmanager_config.time_intervals);
// Check that the last mute_time_interval is the one we just submitted via the form
expect(lastAdded?.name).toEqual('maintenance period');
});
it('creates a new mute timing, with time_intervals in config', async () => {
const capture = captureRequests();
setAlertmanagerConfig(dataSources.am.uid, defaultConfigWithNewTimeIntervalsField);
renderMuteTimings({ pathname: '/alerting/routes/new', search: `?alertmanager=${dataSources.am.name}` });
await fillOutForm({
name: 'maintenance period',
startsAt: '22:01',
endsAt: '24:00',
days: '-1',
months: 'january, july',
});
await saveMuteTiming();
await expectToHaveRedirectedToRoutesRoute();
const requests = await capture;
const alertmanagerUpdate = await getAlertmanagerConfigUpdate(requests);
const lastAdded = last(alertmanagerUpdate.alertmanager_config.time_intervals);
expect(lastAdded?.name).toEqual('maintenance period');
});
it('creates a new mute timing, with time_intervals and mute_time_intervals in config', async () => {
setAlertmanagerConfig(dataSources.am.uid, defaultConfigWithBothTimeIntervalsField);
renderMuteTimings({ pathname: '/alerting/routes/new', search: `?alertmanager=${dataSources.am.name}` });
expect(ui.nameField.get()).toBeInTheDocument();
await fillOutForm({
name: 'maintenance period',
startsAt: '22:00',
endsAt: '24:00',
days: '-1',
months: 'january, july',
});
await saveMuteTiming();
await expectToHaveRedirectedToRoutesRoute();
});
it('prepopulates the form when editing a mute timing', async () => {
const capture = captureRequests();
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?muteName=${encodeURIComponent(muteTimeInterval.name)}`,
});
expect(await ui.nameField.find()).toBeInTheDocument();
expect(ui.nameField.get()).toHaveValue(muteTimeInterval.name);
expect(ui.months.get()).toHaveValue(muteTimeInterval.time_intervals[0].months?.join(', '));
await userEvent.clear(ui.startsAt.getAll()?.[0]);
await userEvent.clear(ui.endsAt.getAll()?.[0]);
await userEvent.clear(ui.days.get());
await userEvent.clear(ui.months.get());
await userEvent.clear(ui.years.get());
const monday = within(ui.weekdays.get()).getByText('Mon');
await userEvent.click(monday);
const formValues = {
days: '-7:-1',
months: '3, 6, 9, 12',
years: '2021:2024',
};
await fillOutForm(formValues);
await saveMuteTiming();
await expectToHaveRedirectedToRoutesRoute();
const requests = await capture;
const alertmanagerUpdate = await getAlertmanagerConfigUpdate(requests);
const lastAdded = last(alertmanagerUpdate.alertmanager_config.mute_time_intervals);
expect(lastAdded?.time_intervals[0]).toMatchObject({
days_of_month: [formValues.days],
months: formValues.months.split(', '),
years: [formValues.years],
});
});
it('form is invalid with duplicate mute timing name', async () => {
renderMuteTimings('/alerting/routes/new');
await fillOutForm({ name: muteTimeInterval.name, days: '1' });
await saveMuteTiming();
expect(await screen.findByRole('alert')).toBeInTheDocument();
});
it('replaces mute timings in routes when the mute timing name is changed', async () => {
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?muteName=${encodeURIComponent(muteTimeInterval.name)}`,
});
expect(await ui.nameField.find()).toBeInTheDocument();
expect(ui.nameField.get()).toHaveValue(muteTimeInterval.name);
await userEvent.clear(ui.nameField.get());
await fillOutForm({ name: 'Lunch breaks' });
await saveMuteTiming();
await expectToHaveRedirectedToRoutesRoute();
});
it('shows error when mute timing does not exist', async () => {
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${'does not exist'}`,
});
expect(await screen.findByText(/No matching mute timing found/i)).toBeInTheDocument();
});
describe('alertingApiServer feature toggle', () => {
beforeEach(() => {
config.featureToggles.alertingApiServer = true;
});
it('allows creation of new mute timings', async () => {
renderMuteTimings('/alerting/routes/new');
await fillOutForm({ name: 'a new mute timing' });
await saveMuteTiming();
await expectToHaveRedirectedToRoutesRoute();
});
it('shows error when mute timing does not exist', async () => {
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_NAME_HAPPY_PATH + '_force_breakage'}`,
});
expect(await screen.findByText(/No matching mute timing found/i)).toBeInTheDocument();
});
it('loads edit form correctly and allows saving', async () => {
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_NAME_HAPPY_PATH}`,
});
await saveMuteTiming();
await expectToHaveRedirectedToRoutesRoute();
});
it('loads view form for provisioned interval', async () => {
renderMuteTimings({
pathname: '/alerting/routes/edit',
search: `?muteName=${TIME_INTERVAL_NAME_FILE_PROVISIONED}`,
});
expect(await screen.findByText(/This mute timing cannot be edited through the UI/i)).toBeInTheDocument();
});
});
});