import { SerializedError } from '@reduxjs/toolkit';
import { prettyDOM, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { setupServer } from 'msw/node';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { byRole, byTestId, byText } from 'testing-library-selector';
import { PluginExtensionTypes, PluginMeta } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import {
DataSourceSrv,
getPluginLinkExtensions,
locationService,
setBackendSrv,
setDataSourceSrv,
usePluginLinkExtensions,
} from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
import {
mockAlertRuleApi,
mockApi,
mockFolderApi,
mockSearchApi,
mockUserApi,
} from 'app/features/alerting/unified/mockApi';
import { mockAlertmanagerChoiceResponse } from 'app/features/alerting/unified/mocks/alertmanagerApi';
import * as actions from 'app/features/alerting/unified/state/actions';
import { getMockUser } from 'app/features/users/__mocks__/userMocks';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { PromAlertingRuleState, PromApplication } from 'app/types/unified-alerting-dto';
import * as analytics from './Analytics';
import RuleList from './RuleList';
import { discoverFeatures } from './api/buildInfo';
import { fetchRules } from './api/prometheus';
import { deleteNamespace, deleteRulerRulesGroup, fetchRulerRules, setRulerRuleGroup } from './api/ruler';
import {
MockDataSourceSrv,
grantUserPermissions,
mockDataSource,
mockPromAlert,
mockPromAlertingRule,
mockPromRecordingRule,
mockPromRuleGroup,
mockPromRuleNamespace,
pausedPromRules,
getPotentiallyPausedRulerRules,
somePromRules,
someRulerRules,
mockFolder,
} from './mocks';
import * as config from './utils/config';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getPluginLinkExtensions: jest.fn(),
usePluginLinkExtensions: jest.fn(),
useReturnToPrevious: jest.fn(),
}));
jest.mock('./api/buildInfo');
jest.mock('./api/prometheus');
jest.mock('./api/ruler');
jest.mock('../../../core/hooks/useMediaQueryChange');
jest.spyOn(ruleActionButtons, 'matchesWidth').mockReturnValue(false);
jest.mock('app/core/core', () => ({
...jest.requireActual('app/core/core'),
appEvents: {
subscribe: () => {
return { unsubscribe: () => {} };
},
emit: () => {},
},
}));
jest.spyOn(analytics, 'logInfo');
jest.spyOn(config, 'getAllDataSources');
jest.spyOn(actions, 'rulesInSameGroupHaveInvalidFor').mockReturnValue([]);
const mocks = {
getAllDataSourcesMock: jest.mocked(config.getAllDataSources),
getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions),
usePluginLinkExtensionsMock: jest.mocked(usePluginLinkExtensions),
rulesInSameGroupHaveInvalidForMock: jest.mocked(actions.rulesInSameGroupHaveInvalidFor),
api: {
discoverFeatures: jest.mocked(discoverFeatures),
fetchRules: jest.mocked(fetchRules),
fetchRulerRules: jest.mocked(fetchRulerRules),
deleteGroup: jest.mocked(deleteRulerRulesGroup),
deleteNamespace: jest.mocked(deleteNamespace),
setRulerRuleGroup: jest.mocked(setRulerRuleGroup),
},
};
const renderRuleList = () => {
locationService.push('/');
return render(
);
};
const dataSources = {
prom: mockDataSource({
name: 'Prometheus',
type: DataSourceType.Prometheus,
}),
promdisabled: mockDataSource({
name: 'Prometheus-disabled',
type: DataSourceType.Prometheus,
jsonData: {
manageAlerts: false,
},
}),
loki: mockDataSource({
name: 'Loki',
type: DataSourceType.Loki,
}),
promBroken: mockDataSource({
name: 'Prometheus-broken',
type: DataSourceType.Prometheus,
}),
};
const ui = {
ruleGroup: byTestId('rule-group'),
pausedRuleGroup: byText(/groupPaused/),
cloudRulesSourceErrors: byTestId('cloud-rulessource-errors'),
groupCollapseToggle: byTestId(selectors.components.AlertRules.groupToggle),
ruleCollapseToggle: byTestId(selectors.components.AlertRules.toggle),
rulesTable: byTestId('rules-table'),
ruleRow: byTestId('row'),
expandedContent: byTestId(selectors.components.AlertRules.expandedContent),
rulesFilterInput: byTestId('search-query-input'),
moreErrorsButton: byRole('button', { name: /more errors/ }),
editCloudGroupIcon: byTestId('edit-group'),
newRuleButton: byText(/new alert rule/i),
exportButton: byText(/export rules/i),
editGroupModal: {
dialog: byRole('dialog'),
namespaceInput: byRole('textbox', { name: /^Namespace/ }),
ruleGroupInput: byRole('textbox', { name: /Evaluation group/ }),
intervalInput: byRole('textbox', {
name: /Evaluation interval How often is the rule evaluated. Applies to every rule within the group./i,
}),
saveButton: byRole('button', { name: /Save/ }),
},
stateTags: {
paused: byText(/^Paused/),
},
actionButtons: {
more: byRole('button', { name: 'More' }),
},
moreActionItems: {
pause: byRole('menuitem', { name: /pause evaluation/i }),
resume: byRole('menuitem', { name: /resume evaluation/i }),
},
};
const server = setupServer();
const configureMockServer = () => {
mockSearchApi(server).search([]);
mockUserApi(server).user(getMockUser());
mockFolderApi(server).folder(
'NAMESPACE_UID',
mockFolder({
accessControl: { [AccessControlAction.AlertingRuleUpdate]: true },
})
);
mockApi(server).plugins.getPluginSettings(
// We aren't particularly concerned with the plugin response in these tests
// at the time of writing, so we can go unknown -> PluginMeta to get the bare minimum
{ id: 'grafana-incident-app' } as unknown as PluginMeta
);
mockAlertmanagerChoiceResponse(server, {
alertmanagersChoice: AlertmanagerChoice.All,
numExternalAlertmanagers: 1,
});
mockAlertRuleApi(server).updateRule('grafana', {
message: 'rule group updated successfully',
updated: ['foo', 'bar', 'baz'],
});
};
beforeAll(() => {
setBackendSrv(backendSrv);
});
describe('RuleList', () => {
beforeEach(() => {
server.listen({ onUnhandledRequest: 'error' });
configureMockServer();
grantUserPermissions([
AccessControlAction.AlertingRuleRead,
AccessControlAction.AlertingRuleUpdate,
AccessControlAction.AlertingRuleExternalRead,
AccessControlAction.AlertingRuleExternalWrite,
]);
mocks.rulesInSameGroupHaveInvalidForMock.mockReturnValue([]);
mocks.usePluginLinkExtensionsMock.mockReturnValue({
extensions: [
{
pluginId: 'grafana-ml-app',
id: '1',
type: PluginExtensionTypes.link,
title: 'Run investigation',
category: 'Sift',
description: 'Run a Sift investigation for this alert',
onClick: jest.fn(),
},
],
isLoading: false,
});
});
afterEach(() => {
server.resetHandlers();
jest.resetAllMocks();
setDataSourceSrv(undefined as unknown as DataSourceSrv);
});
afterAll(() => {
server.close();
});
it('load & show rule groups from multiple cloud data sources', async () => {
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
setDataSourceSrv(new MockDataSourceSrv(dataSources));
mocks.api.discoverFeatures.mockResolvedValue({
application: PromApplication.Prometheus,
features: {
rulerApiEnabled: true,
},
});
mocks.api.fetchRules.mockImplementation((dataSourceName: string) => {
if (dataSourceName === dataSources.prom.name) {
return Promise.resolve([
mockPromRuleNamespace({
name: 'default',
dataSourceName: dataSources.prom.name,
groups: [
mockPromRuleGroup({
name: 'group-2',
}),
mockPromRuleGroup({
name: 'group-1',
}),
],
}),
]);
} else if (dataSourceName === dataSources.loki.name) {
return Promise.resolve([
mockPromRuleNamespace({
name: 'default',
dataSourceName: dataSources.loki.name,
groups: [
mockPromRuleGroup({
name: 'group-1',
}),
],
}),
mockPromRuleNamespace({
name: 'lokins',
dataSourceName: dataSources.loki.name,
groups: [
mockPromRuleGroup({
name: 'group-1',
}),
],
}),
]);
} else if (dataSourceName === dataSources.promBroken.name) {
return Promise.reject({ message: 'this datasource is broken' } as SerializedError);
} else if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) {
return Promise.resolve([
mockPromRuleNamespace({
name: 'foofolder',
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
groups: [
mockPromRuleGroup({
name: 'grafana-group',
rules: [
mockPromAlertingRule({
query: '[]',
}),
],
}),
],
}),
]);
}
return Promise.reject(new Error(`unexpected datasourceName: ${dataSourceName}`));
});
mocks.api.fetchRulerRules.mockRejectedValue({ status: 500, data: { message: 'Server error' } });
await renderRuleList();
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(4));
const groups = await ui.ruleGroup.findAll();
expect(groups).toHaveLength(5);
expect(groups[0]).toHaveTextContent('foofolder');
expect(groups[1]).toHaveTextContent('default group-1');
expect(groups[2]).toHaveTextContent('default group-1');
expect(groups[3]).toHaveTextContent('default group-2');
expect(groups[4]).toHaveTextContent('lokins group-1');
const errors = await ui.cloudRulesSourceErrors.find();
expect(errors).not.toHaveTextContent(
'Failed to load rules state from Prometheus-broken: this datasource is broken'
);
await userEvent.click(ui.moreErrorsButton.get());
expect(errors).toHaveTextContent('Failed to load rules state from Prometheus-broken: this datasource is broken');
});
it('expand rule group, rule and alert details', async () => {
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
mocks.api.discoverFeatures.mockResolvedValue({
application: PromApplication.Cortex,
features: {
rulerApiEnabled: true,
},
});
mocks.api.fetchRules.mockImplementation((dataSourceName: string) => {
if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) {
return Promise.resolve([]);
} else {
return Promise.resolve([
mockPromRuleNamespace({
groups: [
mockPromRuleGroup({
name: 'group-1',
}),
mockPromRuleGroup({
name: 'group-2',
rules: [
mockPromRecordingRule({
name: 'recordingrule',
}),
mockPromAlertingRule({
name: 'alertingrule',
labels: {
severity: 'warning',
foo: 'bar',
},
query: 'topk(5, foo)[5m]',
annotations: {
message: 'great alert',
},
alerts: [
mockPromAlert({
labels: {
foo: 'bar',
severity: 'warning',
},
value: '2e+10',
annotations: {
message: 'first alert message',
},
}),
mockPromAlert({
labels: {
foo: 'baz',
severity: 'error',
},
value: '3e+11',
annotations: {
message: 'first alert message',
},
}),
],
}),
mockPromAlertingRule({
name: 'p-rule',
alerts: [],
state: PromAlertingRuleState.Pending,
}),
mockPromAlertingRule({
name: 'i-rule',
alerts: [],
state: PromAlertingRuleState.Inactive,
}),
],
}),
],
}),
]);
}
});
await renderRuleList();
const groups = await ui.ruleGroup.findAll();
expect(groups).toHaveLength(2);
await waitFor(() => expect(groups[0]).toHaveTextContent(/firing|pending|normal/));
await waitFor(() => expect(groups[1]).toHaveTextContent(/firing|pending|normal/));
expect(groups[0]).toHaveTextContent('1 firing');
expect(groups[1]).toHaveTextContent('1 firing');
expect(groups[1]).toHaveTextContent('1 pending');
expect(groups[1]).toHaveTextContent('1 recording');
expect(groups[1]).toHaveTextContent('1 normal');
// expand second group to see rules table
expect(ui.rulesTable.query()).not.toBeInTheDocument();
await userEvent.click(ui.groupCollapseToggle.get(groups[1]));
const table = await ui.rulesTable.find(groups[1]);
// check that rule rows are rendered properly
let ruleRows = ui.ruleRow.getAll(table);
expect(ruleRows).toHaveLength(4);
expect(ruleRows[0]).toHaveTextContent('Recording rule');
expect(ruleRows[0]).toHaveTextContent('recordingrule');
expect(ruleRows[1]).toHaveTextContent('Firing');
expect(ruleRows[1]).toHaveTextContent('alertingrule');
expect(ruleRows[2]).toHaveTextContent('Pending');
expect(ruleRows[2]).toHaveTextContent('p-rule');
expect(ruleRows[3]).toHaveTextContent('Normal');
expect(ruleRows[3]).toHaveTextContent('i-rule');
expect(byText('Labels').query()).not.toBeInTheDocument();
// expand alert details
await userEvent.click(ui.ruleCollapseToggle.get(ruleRows[1]));
const ruleDetails = ui.expandedContent.get(ruleRows[1]);
const labels = byTestId('label-value').getAll(ruleDetails);
expect(labels[0]).toHaveTextContent('severitywarning');
expect(labels[1]).toHaveTextContent('foobar');
expect(ruleDetails).toHaveTextContent('Expressiontopk ( 5 , foo ) [ 5m ]');
expect(ruleDetails).toHaveTextContent('messagegreat alert');
expect(ruleDetails).toHaveTextContent('Matching instances');
// finally, check instances table
const instancesTable = byTestId('dynamic-table').get(ruleDetails);
expect(instancesTable).toBeInTheDocument();
const instanceRows = byTestId('row').getAll(instancesTable);
expect(instanceRows).toHaveLength(2);
expect(instanceRows![0]).toHaveTextContent('Firingfoobarseveritywarning2021-03-18 08:47:05');
expect(instanceRows![1]).toHaveTextContent('Firingfoobazseverityerror2021-03-18 08:47:05');
// expand details of an instance
await userEvent.click(ui.ruleCollapseToggle.get(instanceRows![0]));
const alertDetails = byTestId(selectors.components.AlertRules.expandedContent).get(instanceRows[0]);
expect(alertDetails).toHaveTextContent('Value2e+10');
expect(alertDetails).toHaveTextContent('messagefirst alert message');
// collapse everything again
await userEvent.click(ui.ruleCollapseToggle.get(instanceRows![0]));
expect(byTestId(selectors.components.AlertRules.expandedContent).query(instanceRows[0])).not.toBeInTheDocument();
await userEvent.click(ui.ruleCollapseToggle.getAll(ruleRows[1])[0]);
await userEvent.click(ui.groupCollapseToggle.get(groups[1]));
expect(ui.rulesTable.query()).not.toBeInTheDocument();
});
it('filters rules and alerts by labels', async () => {
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
mocks.api.discoverFeatures.mockResolvedValue({
application: PromApplication.Cortex,
features: {
rulerApiEnabled: true,
},
});
mocks.api.fetchRulerRules.mockResolvedValue({});
mocks.api.fetchRules.mockImplementation((dataSourceName: string) => {
if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) {
return Promise.resolve([]);
} else {
return Promise.resolve([
mockPromRuleNamespace({
groups: [
mockPromRuleGroup({
name: 'group-1',
rules: [
mockPromAlertingRule({
name: 'alertingrule',
labels: {
severity: 'warning',
foo: 'bar',
},
query: 'topk(5, foo)[5m]',
annotations: {
message: 'great alert',
},
alerts: [
mockPromAlert({
labels: {
foo: 'bar',
severity: 'warning',
},
value: '2e+10',
annotations: {
message: 'first alert message',
},
}),
mockPromAlert({
labels: {
foo: 'baz',
severity: 'error',
},
value: '3e+11',
annotations: {
message: 'first alert message',
},
}),
],
}),
],
}),
mockPromRuleGroup({
name: 'group-2',
rules: [
mockPromAlertingRule({
name: 'alertingrule2',
labels: {
severity: 'error',
foo: 'buzz',
},
query: 'topk(5, foo)[5m]',
annotations: {
message: 'great alert',
},
alerts: [
mockPromAlert({
labels: {
foo: 'buzz',
severity: 'error',
region: 'EU',
},
value: '2e+10',
annotations: {
message: 'alert message',
},
}),
mockPromAlert({
labels: {
foo: 'buzz',
severity: 'error',
region: 'US',
},
value: '3e+11',
annotations: {
message: 'alert message',
},
}),
],
}),
],
}),
],
}),
]);
}
});
await renderRuleList();
const groups = await ui.ruleGroup.findAll();
expect(groups).toHaveLength(2);
const filterInput = ui.rulesFilterInput.get();
await userEvent.type(filterInput, 'label:foo=bar{Enter}');
// Input is debounced so wait for it to be visible
await waitFor(() => expect(filterInput).toHaveValue('label:foo=bar'));
// Group doesn't contain matching labels
await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(1));
await userEvent.click(ui.groupCollapseToggle.get(groups[0]));
const ruleRows = ui.ruleRow.getAll(groups[0]);
expect(ruleRows).toHaveLength(1);
await userEvent.click(ui.ruleCollapseToggle.get(ruleRows[0]));
const ruleDetails = ui.expandedContent.get(ruleRows[0]);
const labels = byTestId('label-value').getAll(ruleDetails);
expect(labels[0]).toHaveTextContent('severitywarning');
expect(labels[1]).toHaveTextContent('foobar');
// Check for different label matchers
await userEvent.clear(filterInput);
await userEvent.type(filterInput, 'label:foo!=bar label:foo!=baz{Enter}');
// Group doesn't contain matching labels
await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(1));
await waitFor(() => expect(ui.ruleGroup.get()).toHaveTextContent('group-2'));
await userEvent.clear(filterInput);
await userEvent.type(filterInput, 'label:"foo=~b.+"{Enter}');
await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(2));
await userEvent.clear(filterInput);
await userEvent.type(filterInput, 'label:region=US{Enter}');
await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(1));
await waitFor(() => expect(ui.ruleGroup.get()).toHaveTextContent('group-2'));
});
describe('pausing rules', () => {
beforeEach(() => {
grantUserPermissions([
AccessControlAction.AlertingRuleRead,
AccessControlAction.AlertingRuleUpdate,
AccessControlAction.AlertingRuleExternalRead,
AccessControlAction.AlertingRuleExternalWrite,
]);
mocks.getAllDataSourcesMock.mockReturnValue([]);
setDataSourceSrv(new MockDataSourceSrv({}));
mocks.api.fetchRulerRules.mockImplementation(() => Promise.resolve(getPotentiallyPausedRulerRules(true)));
mocks.api.fetchRules.mockImplementation((sourceName) =>
Promise.resolve(sourceName === 'grafana' ? pausedPromRules('grafana') : [])
);
});
test('resuming paused alert rule', async () => {
const user = userEvent.setup();
renderRuleList();
// Expand the paused rule group so we can assert the rule state
await user.click(await ui.pausedRuleGroup.find());
expect(await ui.stateTags.paused.find()).toBeInTheDocument();
// TODO: Migrate all testing logic to MSW and so we aren't manually tweaking the API response behaviour
mocks.api.fetchRulerRules.mockImplementationOnce(() => {
return Promise.resolve(getPotentiallyPausedRulerRules(false));
});
await user.click(await ui.actionButtons.more.find());
await user.click(await ui.moreActionItems.resume.find());
await waitFor(() => expect(ui.stateTags.paused.query()).not.toBeInTheDocument());
});
});
describe('edit lotex groups, namespaces', () => {
const testDatasources = {
prom: dataSources.prom,
};
function testCase(name: string, fn: () => Promise) {
it(name, async () => {
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(testDatasources));
setDataSourceSrv(new MockDataSourceSrv(testDatasources));
mocks.api.discoverFeatures.mockResolvedValue({
application: PromApplication.Cortex,
features: {
rulerApiEnabled: true,
},
});
mocks.api.fetchRules.mockImplementation((sourceName) =>
Promise.resolve(sourceName === testDatasources.prom.name ? somePromRules() : [])
);
mocks.api.fetchRulerRules.mockImplementation(({ dataSourceName }) =>
Promise.resolve(dataSourceName === testDatasources.prom.name ? someRulerRules : {})
);
mocks.api.setRulerRuleGroup.mockResolvedValue();
mocks.api.deleteNamespace.mockResolvedValue();
await renderRuleList();
expect(await ui.rulesFilterInput.find()).toHaveValue('');
await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(3));
const groups = await ui.ruleGroup.findAll();
expect(groups).toHaveLength(3);
// open edit dialog
await userEvent.click(ui.editCloudGroupIcon.get(groups[0]));
await waitFor(() => expect(ui.editGroupModal.dialog.get()).toBeInTheDocument());
prettyDOM(ui.editGroupModal.dialog.get());
expect(ui.editGroupModal.namespaceInput.get()).toHaveDisplayValue('namespace1');
expect(ui.editGroupModal.ruleGroupInput.get()).toHaveDisplayValue('group1');
await fn();
});
}
testCase('rename both lotex namespace and group', async () => {
// make changes to form
await userEvent.clear(ui.editGroupModal.namespaceInput.get());
await userEvent.type(ui.editGroupModal.namespaceInput.get(), 'super namespace');
await userEvent.clear(ui.editGroupModal.ruleGroupInput.get());
await userEvent.type(ui.editGroupModal.ruleGroupInput.get(), 'super group');
await userEvent.clear(ui.editGroupModal.intervalInput.get());
await userEvent.type(ui.editGroupModal.intervalInput.get(), '5m');
// submit, check that appropriate calls were made
await userEvent.click(ui.editGroupModal.saveButton.get());
await waitFor(() => expect(ui.editGroupModal.namespaceInput.query()).not.toBeInTheDocument());
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledTimes(2);
expect(mocks.api.deleteNamespace).toHaveBeenCalledTimes(1);
expect(mocks.api.deleteGroup).not.toHaveBeenCalled();
expect(mocks.api.fetchRulerRules).toHaveBeenCalledTimes(4);
expect(mocks.api.setRulerRuleGroup).toHaveBeenNthCalledWith(
1,
{ dataSourceName: testDatasources.prom.name, apiVersion: 'legacy' },
'super namespace',
{
...someRulerRules['namespace1'][0],
name: 'super group',
interval: '5m',
}
);
expect(mocks.api.setRulerRuleGroup).toHaveBeenNthCalledWith(
2,
{ dataSourceName: testDatasources.prom.name, apiVersion: 'legacy' },
'super namespace',
someRulerRules['namespace1'][1]
);
expect(mocks.api.deleteNamespace).toHaveBeenLastCalledWith(
{ dataSourceName: testDatasources.prom.name, apiVersion: 'legacy' },
'namespace1'
);
});
testCase('rename just the lotex group', async () => {
// make changes to form
await userEvent.clear(ui.editGroupModal.ruleGroupInput.get());
await userEvent.type(ui.editGroupModal.ruleGroupInput.get(), 'super group');
await userEvent.clear(ui.editGroupModal.intervalInput.get());
await userEvent.type(ui.editGroupModal.intervalInput.get(), '5m');
// submit, check that appropriate calls were made
await userEvent.click(ui.editGroupModal.saveButton.get());
await waitFor(() => expect(ui.editGroupModal.namespaceInput.query()).not.toBeInTheDocument());
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledTimes(1);
expect(mocks.api.deleteGroup).toHaveBeenCalledTimes(1);
expect(mocks.api.deleteNamespace).not.toHaveBeenCalled();
expect(mocks.api.fetchRulerRules).toHaveBeenCalledTimes(4);
expect(mocks.api.setRulerRuleGroup).toHaveBeenNthCalledWith(
1,
{ dataSourceName: testDatasources.prom.name, apiVersion: 'legacy' },
'namespace1',
{
...someRulerRules['namespace1'][0],
name: 'super group',
interval: '5m',
}
);
expect(mocks.api.deleteGroup).toHaveBeenLastCalledWith(
{ dataSourceName: testDatasources.prom.name, apiVersion: 'legacy' },
'namespace1',
'group1'
);
});
testCase('edit lotex group eval interval, no renaming', async () => {
// make changes to form
await userEvent.clear(ui.editGroupModal.intervalInput.get());
await userEvent.type(ui.editGroupModal.intervalInput.get(), '5m');
// submit, check that appropriate calls were made
await userEvent.click(ui.editGroupModal.saveButton.get());
await waitFor(() => expect(ui.editGroupModal.namespaceInput.query()).not.toBeInTheDocument());
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledTimes(1);
expect(mocks.api.deleteGroup).not.toHaveBeenCalled();
expect(mocks.api.deleteNamespace).not.toHaveBeenCalled();
expect(mocks.api.fetchRulerRules).toHaveBeenCalledTimes(4);
expect(mocks.api.setRulerRuleGroup).toHaveBeenNthCalledWith(
1,
{ dataSourceName: testDatasources.prom.name, apiVersion: 'legacy' },
'namespace1',
{
...someRulerRules['namespace1'][0],
interval: '5m',
}
);
});
});
describe('RBAC Enabled', () => {
describe('Export button', () => {
it('Export button should be visible when the user has alert read permissions', async () => {
grantUserPermissions([AccessControlAction.AlertingRuleRead]);
mocks.getAllDataSourcesMock.mockReturnValue([]);
setDataSourceSrv(new MockDataSourceSrv({}));
mocks.api.fetchRules.mockResolvedValue([
mockPromRuleNamespace({
name: 'foofolder',
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
groups: [
mockPromRuleGroup({
name: 'grafana-group',
rules: [
mockPromAlertingRule({
query: '[]',
}),
],
}),
],
}),
]);
mocks.api.fetchRulerRules.mockResolvedValue({});
renderRuleList();
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1));
expect(ui.exportButton.get()).toBeInTheDocument();
});
});
describe('Grafana Managed Alerts', () => {
it('New alert button should be visible when the user has alert rule create and folder read permissions and no rules exists', async () => {
grantUserPermissions([
AccessControlAction.FoldersRead,
AccessControlAction.AlertingRuleCreate,
AccessControlAction.AlertingRuleRead,
]);
mocks.getAllDataSourcesMock.mockReturnValue([]);
setDataSourceSrv(new MockDataSourceSrv({}));
mocks.api.fetchRules.mockResolvedValue([]);
mocks.api.fetchRulerRules.mockResolvedValue({});
renderRuleList();
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1));
expect(ui.newRuleButton.get()).toBeInTheDocument();
});
it('New alert button should be visible when the user has alert rule create and folder read permissions and rules already exists', async () => {
grantUserPermissions([
AccessControlAction.FoldersRead,
AccessControlAction.AlertingRuleCreate,
AccessControlAction.AlertingRuleRead,
]);
mocks.getAllDataSourcesMock.mockReturnValue([]);
setDataSourceSrv(new MockDataSourceSrv({}));
mocks.api.fetchRules.mockResolvedValue(somePromRules('grafana'));
mocks.api.fetchRulerRules.mockResolvedValue(someRulerRules);
renderRuleList();
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1));
expect(ui.newRuleButton.get()).toBeInTheDocument();
});
});
describe('Cloud Alerts', () => {
it('New alert button should be visible when the user has the alert rule external write and datasource read permissions and no rules exists', async () => {
grantUserPermissions([
// AccessControlAction.AlertingRuleRead,
AccessControlAction.DataSourcesRead,
AccessControlAction.AlertingRuleExternalRead,
AccessControlAction.AlertingRuleExternalWrite,
]);
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
mocks.api.discoverFeatures.mockResolvedValue({
application: PromApplication.Cortex,
features: {
rulerApiEnabled: true,
},
});
mocks.api.fetchRules.mockResolvedValue([]);
mocks.api.fetchRulerRules.mockResolvedValue({});
renderRuleList();
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1));
expect(ui.newRuleButton.get()).toBeInTheDocument();
});
it('New alert button should be visible when the user has the alert rule external write and data source read permissions and rules already exists', async () => {
grantUserPermissions([
AccessControlAction.DataSourcesRead,
AccessControlAction.AlertingRuleExternalRead,
AccessControlAction.AlertingRuleExternalWrite,
]);
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
mocks.api.discoverFeatures.mockResolvedValue({
application: PromApplication.Cortex,
features: {
rulerApiEnabled: true,
},
});
mocks.api.fetchRules.mockResolvedValue(somePromRules('Cortex'));
mocks.api.fetchRulerRules.mockResolvedValue(someRulerRules);
renderRuleList();
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1));
expect(ui.newRuleButton.get()).toBeInTheDocument();
});
});
});
describe('Analytics', () => {
it('Sends log info when creating an alert rule from a scratch', async () => {
grantUserPermissions([
AccessControlAction.FoldersRead,
AccessControlAction.AlertingRuleCreate,
AccessControlAction.AlertingRuleRead,
]);
mocks.getAllDataSourcesMock.mockReturnValue([]);
setDataSourceSrv(new MockDataSourceSrv({}));
mocks.api.fetchRules.mockResolvedValue([]);
mocks.api.fetchRulerRules.mockResolvedValue({});
renderRuleList();
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1));
const button = screen.getByText('New alert rule');
button.addEventListener('click', (event) => event.preventDefault(), false);
expect(button).toBeEnabled();
await userEvent.click(button);
expect(analytics.logInfo).toHaveBeenCalledWith(analytics.LogMessages.alertRuleFromScratch);
});
});
});