import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector';
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import { interceptLinkClicks } from 'app/core/navigation/patch/interceptLinkClicks';
import { contextSrv } from 'app/core/services/context_srv';
import store from 'app/core/store';
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
import { configureStore } from 'app/store/configureStore';
import { AccessControlAction } from 'app/types';
import Receivers from './Receivers';
import { updateAlertManagerConfig, fetchAlertManagerConfig, fetchStatus, testReceivers } from './api/alertmanager';
import { fetchNotifiers } from './api/grafana';
import {
mockDataSource,
MockDataSourceSrv,
someCloudAlertManagerConfig,
someCloudAlertManagerStatus,
someGrafanaAlertManagerConfig,
} from './mocks';
import { grafanaNotifiersMock } from './mocks/grafana-notifiers';
import { getAllDataSources } from './utils/config';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
jest.mock('./api/alertmanager');
jest.mock('./api/grafana');
jest.mock('./utils/config');
jest.mock('app/core/services/context_srv');
const mocks = {
getAllDataSources: jest.mocked(getAllDataSources),
api: {
fetchConfig: jest.mocked(fetchAlertManagerConfig),
fetchStatus: jest.mocked(fetchStatus),
updateConfig: jest.mocked(updateAlertManagerConfig),
fetchNotifiers: jest.mocked(fetchNotifiers),
testReceivers: jest.mocked(testReceivers),
},
contextSrv: jest.mocked(contextSrv),
};
const renderReceivers = (alertManagerSourceName?: string) => {
const store = configureStore();
locationService.push(
'/alerting/notifications' +
(alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : '')
);
return render(
);
};
const dataSources = {
alertManager: mockDataSource({
name: 'CloudManager',
type: DataSourceType.Alertmanager,
}),
promAlertManager: mockDataSource({
name: 'PromManager',
type: DataSourceType.Alertmanager,
jsonData: {
implementation: AlertManagerImplementation.prometheus,
},
}),
};
const ui = {
newContactPointButton: byRole('link', { name: /new contact point/i }),
saveContactButton: byRole('button', { name: /save contact point/i }),
newContactPointTypeButton: byRole('button', { name: /new contact point type/i }),
testContactPointButton: byRole('button', { name: /Test/ }),
testContactPointModal: byRole('heading', { name: /test contact point/i }),
customContactPointOption: byRole('radio', { name: /custom/i }),
contactPointAnnotationSelect: (idx: number) => byTestId(`annotation-key-${idx}`),
contactPointAnnotationValue: (idx: number) => byTestId(`annotation-value-${idx}`),
contactPointLabelKey: (idx: number) => byTestId(`label-key-${idx}`),
contactPointLabelValue: (idx: number) => byTestId(`label-value-${idx}`),
testContactPoint: byRole('button', { name: /send test notification/i }),
cancelButton: byTestId('cancel-button'),
receiversTable: byTestId('receivers-table'),
templatesTable: byTestId('templates-table'),
alertManagerPicker: byTestId('alertmanager-picker'),
channelFormContainer: byTestId('item-container'),
inputs: {
name: byPlaceholderText('Name'),
email: {
addresses: byLabelText(/Addresses/),
toEmails: byLabelText(/To/),
},
hipchat: {
url: byLabelText('Hip Chat Url'),
apiKey: byLabelText('API Key'),
},
slack: {
webhookURL: byLabelText(/Webhook URL/i),
},
webhook: {
URL: byLabelText(/The endpoint to send HTTP POST requests to/i),
},
},
};
const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise => {
await userEvent.click(byRole('combobox').get(selectElement));
await selectOptionInTest(selectElement, optionText);
};
document.addEventListener('click', interceptLinkClicks);
describe('Receivers', () => {
beforeEach(() => {
jest.resetAllMocks();
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock);
setDataSourceSrv(new MockDataSourceSrv(dataSources));
mocks.contextSrv.isEditor = true;
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
mocks.contextSrv.evaluatePermission.mockImplementation(() => []);
mocks.contextSrv.hasPermission.mockImplementation((action) => {
const permissions = [
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsWrite,
AccessControlAction.AlertingNotificationsExternalRead,
AccessControlAction.AlertingNotificationsExternalWrite,
];
return permissions.includes(action as AccessControlAction);
});
mocks.contextSrv.hasAccess.mockImplementation(() => true);
});
it('Template and receiver tables are rendered, alertmanager can be selected', async () => {
mocks.api.fetchConfig.mockImplementation((name) =>
Promise.resolve(name === GRAFANA_RULES_SOURCE_NAME ? someGrafanaAlertManagerConfig : someCloudAlertManagerConfig)
);
await renderReceivers();
// check that by default grafana templates & receivers are fetched rendered in appropriate tables
let receiversTable = await ui.receiversTable.find();
let templatesTable = await ui.templatesTable.find();
let templateRows = templatesTable.querySelectorAll('tbody tr');
expect(templateRows).toHaveLength(3);
expect(templateRows[0]).toHaveTextContent('first template');
expect(templateRows[1]).toHaveTextContent('second template');
expect(templateRows[2]).toHaveTextContent('third template');
let receiverRows = receiversTable.querySelectorAll('tbody tr');
expect(receiverRows[0]).toHaveTextContent('default');
expect(receiverRows[1]).toHaveTextContent('critical');
expect(receiverRows).toHaveLength(2);
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1);
expect(mocks.api.fetchConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME);
expect(mocks.api.fetchNotifiers).toHaveBeenCalledTimes(1);
expect(locationService.getSearchObject()[ALERTMANAGER_NAME_QUERY_KEY]).toEqual(undefined);
// select external cloud alertmanager, check that data is retrieved and contents are rendered as appropriate
await clickSelectOption(ui.alertManagerPicker.get(), 'CloudManager');
await byText('cloud-receiver').find();
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(2);
expect(mocks.api.fetchConfig).toHaveBeenLastCalledWith('CloudManager');
receiversTable = await ui.receiversTable.find();
templatesTable = await ui.templatesTable.find();
templateRows = templatesTable.querySelectorAll('tbody tr');
expect(templateRows[0]).toHaveTextContent('foo template');
expect(templateRows).toHaveLength(1);
receiverRows = receiversTable.querySelectorAll('tbody tr');
expect(receiverRows[0]).toHaveTextContent('cloud-receiver');
expect(receiverRows).toHaveLength(1);
expect(locationService.getSearchObject()[ALERTMANAGER_NAME_QUERY_KEY]).toEqual('CloudManager');
});
it('Grafana receiver can be tested', async () => {
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
await renderReceivers();
// go to new contact point page
await userEvent.click(await ui.newContactPointButton.find());
await byRole('heading', { name: /create contact point/i }).find();
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/new');
// type in a name for the new receiver
await userEvent.type(ui.inputs.name.get(), 'my new receiver');
// enter some email
const email = ui.inputs.email.addresses.get();
await userEvent.clear(email);
await userEvent.type(email, 'tester@grafana.com');
// try to test the contact point
await userEvent.click(await ui.testContactPointButton.find());
await waitFor(() => expect(ui.testContactPointModal.get()).toBeInTheDocument(), { timeout: 1000 });
await userEvent.click(ui.customContactPointOption.get());
await waitFor(() => expect(ui.contactPointAnnotationSelect(0).get()).toBeInTheDocument());
// enter custom annotations and labels
await clickSelectOption(ui.contactPointAnnotationSelect(0).get(), 'Description');
await userEvent.type(ui.contactPointAnnotationValue(0).get(), 'Test contact point');
await userEvent.type(ui.contactPointLabelKey(0).get(), 'foo');
await userEvent.type(ui.contactPointLabelValue(0).get(), 'bar');
await userEvent.click(ui.testContactPoint.get());
await waitFor(() => expect(mocks.api.testReceivers).toHaveBeenCalled());
expect(mocks.api.testReceivers).toHaveBeenCalledWith(
'grafana',
[
{
grafana_managed_receiver_configs: [
{
disableResolveMessage: false,
name: 'test',
secureSettings: {},
settings: { addresses: 'tester@grafana.com', singleEmail: false },
type: 'email',
},
],
name: 'test',
},
],
{ annotations: { description: 'Test contact point' }, labels: { foo: 'bar' } }
);
});
it('Grafana receiver can be created', async () => {
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
await renderReceivers();
// go to new contact point page
await userEvent.click(await ui.newContactPointButton.find());
await byRole('heading', { name: /create contact point/i }).find();
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/new');
// type in a name for the new receiver
await userEvent.type(byPlaceholderText('Name').get(), 'my new receiver');
// check that default email form is rendered
await ui.inputs.email.addresses.find();
// select hipchat
await clickSelectOption(byTestId('items.0.type').get(), 'HipChat');
// check that email options are gone and hipchat options appear
expect(ui.inputs.email.addresses.query()).not.toBeInTheDocument();
const urlInput = ui.inputs.hipchat.url.get();
const apiKeyInput = ui.inputs.hipchat.apiKey.get();
await userEvent.type(urlInput, 'http://hipchat');
await userEvent.type(apiKeyInput, 'foobarbaz');
await userEvent.click(await ui.saveContactButton.find());
// see that we're back to main page and proper api calls have been made
await ui.receiversTable.find();
expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1);
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3);
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications');
expect(mocks.api.updateConfig).toHaveBeenLastCalledWith(GRAFANA_RULES_SOURCE_NAME, {
...someGrafanaAlertManagerConfig,
alertmanager_config: {
...someGrafanaAlertManagerConfig.alertmanager_config,
receivers: [
...(someGrafanaAlertManagerConfig.alertmanager_config.receivers ?? []),
{
name: 'my new receiver',
grafana_managed_receiver_configs: [
{
disableResolveMessage: false,
name: 'my new receiver',
secureSettings: {},
settings: {
apiKey: 'foobarbaz',
url: 'http://hipchat',
},
type: 'hipchat',
},
],
},
],
},
});
});
it('Hides create contact point button for users without permission', () => {
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
mocks.contextSrv.hasAccess.mockImplementation((action) =>
[AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsExternalRead].some(
(a) => a === action
)
);
renderReceivers();
expect(ui.newContactPointButton.query()).not.toBeInTheDocument();
});
it('Cloud alertmanager receiver can be edited', async () => {
mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
await renderReceivers('CloudManager');
// click edit button for the receiver
const receiversTable = await ui.receiversTable.find();
const receiverRows = receiversTable.querySelectorAll('tbody tr');
expect(receiverRows[0]).toHaveTextContent('cloud-receiver');
await userEvent.click(byTestId('edit').get(receiverRows[0]));
// check that form is open
await byRole('heading', { name: /update contact point/i }).find();
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/cloud-receiver/edit');
expect(ui.channelFormContainer.queryAll()).toHaveLength(2);
// delete the email channel
expect(ui.channelFormContainer.queryAll()).toHaveLength(2);
await userEvent.click(byTestId('items.0.delete-button').get());
expect(ui.channelFormContainer.queryAll()).toHaveLength(1);
// modify webhook url
const slackContainer = ui.channelFormContainer.get();
await userEvent.click(byText('Optional Slack settings').get(slackContainer));
await userEvent.type(ui.inputs.slack.webhookURL.get(slackContainer), 'http://newgreaturl');
// add confirm button to action
await userEvent.click(byText(/Actions \(1\)/i).get(slackContainer));
await userEvent.click(await byTestId('items.1.settings.actions.0.confirm.add-button').find());
const confirmSubform = byTestId('items.1.settings.actions.0.confirm.container').get();
await userEvent.type(byLabelText('Text').get(confirmSubform), 'confirm this');
// delete a field
await userEvent.click(byText(/Fields \(2\)/i).get(slackContainer));
await userEvent.click(byTestId('items.1.settings.fields.0.delete-button').get());
await byText(/Fields \(1\)/i).get(slackContainer);
// add another channel
await userEvent.click(ui.newContactPointTypeButton.get());
await clickSelectOption(await byTestId('items.2.type').find(), 'Webhook');
await userEvent.type(await ui.inputs.webhook.URL.find(), 'http://webhookurl');
await userEvent.click(ui.saveContactButton.get());
// see that we're back to main page and proper api calls have been made
await ui.receiversTable.find();
expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1);
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3);
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications');
expect(mocks.api.updateConfig).toHaveBeenLastCalledWith('CloudManager', {
...someCloudAlertManagerConfig,
alertmanager_config: {
...someCloudAlertManagerConfig.alertmanager_config,
receivers: [
{
name: 'cloud-receiver',
slack_configs: [
{
actions: [
{
confirm: {
text: 'confirm this',
},
text: 'action1text',
type: 'action1type',
url: 'http://action1',
},
],
api_url: 'http://slack1http://newgreaturl',
channel: '#mychannel',
fields: [
{
short: false,
title: 'field2',
value: 'text2',
},
],
link_names: false,
send_resolved: false,
short_fields: false,
},
],
webhook_configs: [
{
send_resolved: true,
url: 'http://webhookurl',
},
],
},
],
},
});
});
it('Prometheus Alertmanager receiver cannot be edited', async () => {
mocks.api.fetchStatus.mockResolvedValue({
...someCloudAlertManagerStatus,
config: someCloudAlertManagerConfig.alertmanager_config,
});
await renderReceivers(dataSources.promAlertManager.name);
const receiversTable = await ui.receiversTable.find();
// there's no templates table for vanilla prom, API does not return templates
expect(ui.templatesTable.query()).not.toBeInTheDocument();
// click view button on the receiver
const receiverRows = receiversTable.querySelectorAll('tbody tr');
expect(receiverRows[0]).toHaveTextContent('cloud-receiver');
expect(byTestId('edit').query(receiverRows[0])).not.toBeInTheDocument();
await userEvent.click(byTestId('view').get(receiverRows[0]));
// check that form is open
await byRole('heading', { name: /contact point/i }).find();
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/cloud-receiver/edit');
const channelForms = ui.channelFormContainer.queryAll();
expect(channelForms).toHaveLength(2);
// check that inputs are disabled and there is no save button
expect(ui.inputs.name.queryAll()[0]).toHaveAttribute('readonly');
expect(ui.inputs.email.toEmails.get(channelForms[0])).toHaveAttribute('readonly');
expect(ui.inputs.slack.webhookURL.get(channelForms[1])).toHaveAttribute('readonly');
expect(ui.newContactPointButton.query()).not.toBeInTheDocument();
expect(ui.testContactPointButton.query()).not.toBeInTheDocument();
expect(ui.saveContactButton.query()).not.toBeInTheDocument();
expect(ui.cancelButton.query()).toBeInTheDocument();
expect(mocks.api.fetchConfig).not.toHaveBeenCalled();
expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1);
});
it('Loads config from status endpoint if there is no user config', async () => {
// loading an empty config with make it fetch config from status endpoint
mocks.api.fetchConfig.mockResolvedValue({
template_files: {},
alertmanager_config: {},
});
mocks.api.fetchStatus.mockResolvedValue(someCloudAlertManagerStatus);
await renderReceivers('CloudManager');
// check that receiver from the default config is represented
const receiversTable = await ui.receiversTable.find();
const receiverRows = receiversTable.querySelectorAll('tbody tr');
expect(receiverRows[0]).toHaveTextContent('default-email');
// check that both config and status endpoints were called
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1);
expect(mocks.api.fetchConfig).toHaveBeenLastCalledWith('CloudManager');
expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1);
expect(mocks.api.fetchStatus).toHaveBeenLastCalledWith('CloudManager');
});
});