grafana_bak/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx
2025-04-01 10:38:02 +09:00

540 lines
19 KiB
TypeScript

import { produce } from 'immer';
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
import { render, screen, userEvent, within } from 'test/test-utils';
import { byLabelText, byRole, byTestId } from 'testing-library-selector';
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
import { PERMISSIONS_NOTIFICATION_POLICIES } from 'app/features/alerting/unified/components/notification-policies/permissions';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import {
getErrorResponse,
makeAllAlertmanagerConfigFetchFail,
makeAllK8sGetEndpointsFail,
} from 'app/features/alerting/unified/mocks/server/configure';
import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants';
import {
getAlertmanagerConfig,
setAlertmanagerConfig,
setAlertmanagerStatus,
} from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import {
TIME_INTERVAL_NAME_FILE_PROVISIONED,
TIME_INTERVAL_NAME_HAPPY_PATH,
} from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s';
import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import {
AlertManagerCortexConfig,
AlertManagerDataSourceJsonData,
AlertManagerImplementation,
MatcherOperator,
RouteWithID,
} from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import NotificationPolicies from './NotificationPoliciesPage';
import { findRoutesMatchingFilters } from './components/notification-policies/NotificationPoliciesList';
import {
grantUserPermissions,
mockDataSource,
someCloudAlertManagerConfig,
someCloudAlertManagerStatus,
} from './mocks';
import { ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
jest.mock('./useRouteGroupsMatcher');
setupMswServer();
const updateTiming = async (selectElement: HTMLElement, value: string): Promise<void> => {
const user = userEvent.setup();
const input = byRole('textbox').get(selectElement);
await user.clear(input);
await user.type(input, value);
};
const openDefaultPolicyEditModal = async () => {
const user = userEvent.setup();
await user.click(await ui.moreActionsDefaultPolicy.find());
await user.click(await ui.editButton.find());
};
const openEditModal = async (
/** (zero-based) Index of the policy in the list to open the edit modal for */
index: number
) => {
const user = userEvent.setup();
await user.click((await ui.moreActions.findAll())[index]);
await user.click(await ui.editButton.find());
};
const renderNotificationPolicies = (alertManagerSourceName: string = GRAFANA_RULES_SOURCE_NAME) =>
render(
<>
<AppNotificationList />
<NotificationPolicies />
</>,
{
historyOptions: {
initialEntries: [
'/alerting/routes' +
(alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : ''),
],
},
}
);
const dataSources = {
am: mockDataSource({
name: 'Alertmanager',
type: DataSourceType.Alertmanager,
}),
promAlertManager: mockDataSource<AlertManagerDataSourceJsonData>({
name: 'PromManager',
type: DataSourceType.Alertmanager,
uid: 'prometheusAlertManager',
jsonData: {
implementation: AlertManagerImplementation.prometheus,
},
}),
mimir: mockDataSource<AlertManagerDataSourceJsonData>({
name: 'MimirAlertmanager',
type: DataSourceType.Alertmanager,
uid: MIMIR_DATASOURCE_UID,
jsonData: {
implementation: AlertManagerImplementation.mimir,
},
}),
};
const ui = {
/** Row of policy tree containing default policy */
rootRouteContainer: byTestId('am-root-route-container'),
/** (deeply) Nested rows of policies under the default/root policy */
row: byTestId('am-route-container'),
newChildPolicyButton: byRole('button', { name: /New child policy/ }),
newSiblingPolicyButton: byRole('button', { name: /Add new policy/ }),
moreActionsDefaultPolicy: byLabelText(/more actions for default policy/i),
moreActions: byLabelText(/more actions for policy/i),
editButton: byRole('menuitem', { name: 'Edit' }),
saveButton: byRole('button', { name: /update (default )?policy/i }),
deleteRouteButton: byRole('menuitem', { name: 'Delete' }),
receiverSelect: byTestId('am-receiver-select'),
groupSelect: byTestId('am-group-select'),
muteTimingSelect: byTestId('am-mute-timing-select'),
groupWaitContainer: byTestId('am-group-wait'),
groupIntervalContainer: byTestId('am-group-interval'),
groupRepeatContainer: byTestId('am-repeat-interval'),
confirmDeleteModal: byRole('dialog'),
confirmDeleteButton: byRole('button', { name: /yes, delete policy/i }),
};
const getRootRoute = async () => {
return ui.rootRouteContainer.find();
};
describe.each([
// k8s API enabled
true,
// k8s API disabled
false,
])('NotificationPolicies with alertingApiServer=%p', (apiServerEnabled) => {
apiServerEnabled ? testWithFeatureToggles(['alertingApiServer']) : testWithFeatureToggles([]);
beforeEach(() => {
setupDataSources(...Object.values(dataSources));
grantUserPermissions([
AccessControlAction.AlertingInstanceRead,
AccessControlAction.AlertingInstanceCreate,
AccessControlAction.AlertingInstanceUpdate,
AccessControlAction.AlertingInstancesExternalRead,
AccessControlAction.AlertingInstancesExternalWrite,
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsWrite,
AccessControlAction.AlertingNotificationsExternalRead,
AccessControlAction.AlertingNotificationsExternalWrite,
...PERMISSIONS_NOTIFICATION_POLICIES,
]);
});
it('loads and shows routes', async () => {
const { alertmanager_config: testConfig } = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
const { route: defaultRoute } = testConfig;
renderNotificationPolicies();
const rootRouteEl = await getRootRoute();
expect(rootRouteEl).toHaveTextContent(new RegExp(`delivered to ${defaultRoute?.receiver}`, 'i'));
expect(rootRouteEl).toHaveTextContent(new RegExp(`grouped by ${defaultRoute?.group_by?.join(', ')}`, 'i'));
expect(rootRouteEl).toHaveTextContent(/wait 30s to group/i);
expect(rootRouteEl).toHaveTextContent(/wait 5m before sending/i);
expect(rootRouteEl).toHaveTextContent(/repeated every 4h/i);
const rows = await ui.row.findAll();
expect(rows).toHaveLength(5);
defaultRoute?.routes?.forEach((route) => {
Object.entries(route.match ?? {}).forEach(([label, value]) => {
expect(screen.getByText(`${label} = ${value}`)).toBeInTheDocument();
});
Object.entries(route.match_re ?? {}).forEach(([label, value]) => {
expect(screen.getByText(`${label} =~ ${value}`)).toBeInTheDocument();
});
if (route.group_by) {
expect(rows.some((row) => row?.textContent?.includes(`Grouped by ${route.group_by?.join(', ')}`))).toBe(true);
}
if (route.receiver) {
expect(rows.some((row) => row?.textContent?.includes(`Delivered to ${route.receiver}`))).toBe(true);
}
});
});
it('can edit root route if one is already defined', async () => {
const { user } = renderNotificationPolicies();
let rootRoute = await getRootRoute();
expect(rootRoute).toHaveTextContent('default policy');
expect(rootRoute).toHaveTextContent(/delivered to grafana-default-email/i);
expect(rootRoute).toHaveTextContent(/grouped by alertname/i);
await openDefaultPolicyEditModal();
// configure receiver & group by
const receiverSelect = await ui.receiverSelect.find();
// The contact points are fetched from the k8s API, which we aren't overriding here
// when we use a different
await clickSelectOption(receiverSelect, 'lotsa-emails');
const groupSelect = ui.groupSelect.get();
await user.type(byRole('combobox').get(groupSelect), 'namespace{enter}');
// configure timing intervals
await user.click(screen.getByText(/timing options/i));
await updateTiming(ui.groupWaitContainer.get(), '1m');
await updateTiming(ui.groupIntervalContainer.get(), '4m');
await updateTiming(ui.groupRepeatContainer.get(), '5h');
//save
await user.click(await screen.findByRole('button', { name: /update default policy/i }));
// wait for it to go out of edit mode
expect(await screen.findByText(/updated notification policies/i)).toBeInTheDocument();
// check that new config values are rendered
rootRoute = await getRootRoute();
expect(rootRoute).toHaveTextContent(/delivered to lotsa-emails/i);
expect(rootRoute).toHaveTextContent(/grouped by alertname, namespace/i);
});
it('can edit root route if one is not defined yet', async () => {
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, {
alertmanager_config: {
route: {},
receivers: [{ name: 'lotsa-emails' }],
},
template_files: {},
});
const { user } = renderNotificationPolicies();
await openDefaultPolicyEditModal();
// configure receiver & group by
const receiverSelect = await ui.receiverSelect.find();
await clickSelectOption(receiverSelect, 'lotsa-emails');
const groupSelect = ui.groupSelect.get();
await user.type(byRole('combobox').get(groupSelect), 'severity{enter}');
await user.type(byRole('combobox').get(groupSelect), 'namespace{enter}');
//save
await user.click(await screen.findByRole('button', { name: /update default policy/i }));
expect(await screen.findByText(/updated notification policies/i)).toBeInTheDocument();
const rootRoute = await getRootRoute();
expect(rootRoute).toHaveTextContent(/delivered to lotsa-emails/i);
expect(rootRoute).toHaveTextContent(/grouped by severity, namespace/i);
});
it('hides create and edit button if user does not have permission', async () => {
grantUserPermissions([
AccessControlAction.AlertingInstanceRead,
AccessControlAction.AlertingInstancesExternalRead,
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsExternalRead,
]);
const { user } = renderNotificationPolicies();
expect(ui.newChildPolicyButton.query()).not.toBeInTheDocument();
expect(ui.newSiblingPolicyButton.query()).not.toBeInTheDocument();
await user.click(await ui.moreActionsDefaultPolicy.find());
expect(ui.editButton.query()).not.toBeInTheDocument();
});
it('Show error message if loading Alertmanager config fails', async () => {
const errMessage = "Alertmanager has exploded. it's gone. Forget about it.";
makeAllAlertmanagerConfigFetchFail(getErrorResponse(errMessage));
makeAllK8sGetEndpointsFail('alerting.config.notfound', errMessage);
renderNotificationPolicies();
const alert = await screen.findByRole('alert', { name: /error loading alertmanager config/i });
expect(await within(alert).findByText(errMessage)).toBeInTheDocument();
expect(ui.rootRouteContainer.query()).not.toBeInTheDocument();
});
it('allows user to reload and update policies if its been changed by another user', async () => {
const { user } = renderNotificationPolicies();
const NEW_INTERVAL = '12h';
await getRootRoute();
const existingConfig = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
const modifiedConfig = produce(existingConfig, (draft) => {
draft.alertmanager_config.route!.group_interval = NEW_INTERVAL;
});
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, modifiedConfig);
await openDefaultPolicyEditModal();
await user.click(await screen.findByRole('button', { name: /update default policy/i }));
expect(
(await screen.findAllByText(/the notification policy tree has been updated by another user/i))[0]
).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /cancel/i }));
await user.click(screen.getByRole('button', { name: /reload policies/i }));
expect((await screen.findAllByTestId('timing-options'))[0]).toHaveTextContent(NEW_INTERVAL);
});
it('Should be able to delete an empty route', async () => {
const defaultConfig: AlertManagerCortexConfig = {
alertmanager_config: {
route: {
routes: [{}],
},
},
template_files: {},
};
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, defaultConfig);
const { user } = renderNotificationPolicies(GRAFANA_RULES_SOURCE_NAME);
await user.click(await ui.moreActions.find());
const deleteButtons = await ui.deleteRouteButton.find();
await user.click(deleteButtons);
const confirmDeleteButton = ui.confirmDeleteButton.get(ui.confirmDeleteModal.get());
expect(confirmDeleteButton).toBeInTheDocument();
await user.click(confirmDeleteButton);
expect(await screen.findByRole('status')).toHaveTextContent(/updated notification policies/i);
expect(ui.row.query()).not.toBeInTheDocument();
});
it('Can add a mute timing to a route', async () => {
const { user } = renderNotificationPolicies();
await openEditModal(0);
const muteTimingSelect = ui.muteTimingSelect.get();
await clickSelectOption(muteTimingSelect, TIME_INTERVAL_NAME_HAPPY_PATH);
await clickSelectOption(muteTimingSelect, TIME_INTERVAL_NAME_FILE_PROVISIONED);
await user.click(ui.saveButton.get());
expect(await screen.findByRole('status')).toHaveTextContent(/updated notification policies/i);
const policy = (await ui.row.findAll())[0];
expect(policy).toHaveTextContent(
`Muted when ${TIME_INTERVAL_NAME_HAPPY_PATH}, ${TIME_INTERVAL_NAME_FILE_PROVISIONED}`
);
});
});
describe('Grafana alertmanager - config API', () => {
it('Converts matchers to object_matchers for grafana alertmanager', async () => {
const { user } = renderNotificationPolicies();
const policyIndex = 0;
await openEditModal(policyIndex);
// Save policy to test that format is converted to object_matchers
await user.click(await ui.saveButton.find());
expect(await screen.findByRole('status')).toHaveTextContent(/updated notification policies/i);
const updatedConfig = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
expect(updatedConfig.alertmanager_config.route?.routes?.[policyIndex].object_matchers).toMatchSnapshot();
});
});
describe('Non-Grafana alertmanagers', () => {
it.skip('Shows an empty config when config returns an error and the AM supports lazy config initialization', async () => {
makeAllAlertmanagerConfigFetchFail(getErrorResponse('alertmanager storage object not found'));
setAlertmanagerStatus(dataSources.mimir.uid, someCloudAlertManagerStatus);
renderNotificationPolicies(dataSources.mimir.name);
expect(await ui.rootRouteContainer.find()).toBeInTheDocument();
});
it('Keeps matchers for non-grafana alertmanager sources', async () => {
setAlertmanagerConfig(dataSources.am.uid, {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
continue: false,
receiver: 'default',
group_by: ['alertname'],
routes: [
{
receiver: 'simple-receiver',
matchers: ['hello=world', 'foo!=bar'],
},
],
group_interval: '4m',
group_wait: '1m',
repeat_interval: '5h',
},
templates: [],
},
template_files: {},
});
const { user } = renderNotificationPolicies(dataSources.am.name);
const policyIndex = 0;
await openEditModal(policyIndex);
// Save policy to test that format is NOT converted
await user.click(await ui.saveButton.find());
const updatedConfig = getAlertmanagerConfig(dataSources.am.uid);
expect(updatedConfig.alertmanager_config.route?.routes?.[policyIndex].matchers).toMatchSnapshot();
});
it('Prometheus Alertmanager routes cannot be edited', async () => {
setAlertmanagerStatus(dataSources.promAlertManager.uid, {
...someCloudAlertManagerStatus,
config: someCloudAlertManagerConfig.alertmanager_config,
});
renderNotificationPolicies(dataSources.promAlertManager.name);
expect(await ui.rootRouteContainer.find()).toBeInTheDocument();
const rows = await ui.row.findAll();
expect(rows).toHaveLength(2);
expect(ui.moreActions.query()).not.toBeInTheDocument();
expect(ui.moreActionsDefaultPolicy.query()).not.toBeInTheDocument();
});
it('Prometheus Alertmanager has no CTA button if there are no specific policies', async () => {
setAlertmanagerStatus(dataSources.promAlertManager.uid, {
...someCloudAlertManagerStatus,
config: {
...someCloudAlertManagerConfig.alertmanager_config,
route: {
...someCloudAlertManagerConfig.alertmanager_config.route,
routes: undefined,
},
},
});
renderNotificationPolicies(dataSources.promAlertManager.name);
expect(await ui.rootRouteContainer.find()).toBeInTheDocument();
expect(ui.newChildPolicyButton.query()).not.toBeInTheDocument();
expect(ui.newSiblingPolicyButton.query()).not.toBeInTheDocument();
});
});
describe('findRoutesMatchingFilters', () => {
const simpleRouteTree: RouteWithID = {
id: '0',
receiver: 'default-receiver',
routes: [
{
id: '1',
receiver: 'simple-receiver',
matchers: ['hello=world', 'foo!=bar'],
routes: [
{
id: '2',
matchers: ['bar=baz'],
},
],
},
],
};
it('should not filter when we do not have any valid filters', () => {
expect(findRoutesMatchingFilters(simpleRouteTree, {})).toHaveProperty('filtersApplied', false);
});
it('should not match non-existing', () => {
expect(
findRoutesMatchingFilters(simpleRouteTree, {
labelMatchersFilter: [['foo', MatcherOperator.equal, 'bar']],
}).matchedRoutesWithPath.size
).toBe(0);
const matchingRoutes = findRoutesMatchingFilters(simpleRouteTree, {
contactPointFilter: 'does-not-exist',
});
expect(matchingRoutes).toMatchSnapshot();
});
it('should work with only label matchers', () => {
const matchingRoutes = findRoutesMatchingFilters(simpleRouteTree, {
labelMatchersFilter: [['hello', MatcherOperator.equal, 'world']],
});
expect(matchingRoutes).toMatchSnapshot();
});
it('should work with only contact point and inheritance', () => {
const matchingRoutes = findRoutesMatchingFilters(simpleRouteTree, {
contactPointFilter: 'simple-receiver',
});
expect(matchingRoutes).toMatchSnapshot();
});
it('should work with non-intersecting filters', () => {
const matchingRoutes = findRoutesMatchingFilters(simpleRouteTree, {
labelMatchersFilter: [['hello', MatcherOperator.equal, 'world']],
contactPointFilter: 'does-not-exist',
});
expect(matchingRoutes).toMatchSnapshot();
});
it('should work with all filters', () => {
const matchingRoutes = findRoutesMatchingFilters(simpleRouteTree, {
labelMatchersFilter: [['hello', MatcherOperator.equal, 'world']],
contactPointFilter: 'simple-receiver',
});
expect(matchingRoutes).toMatchSnapshot();
});
});