123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- 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(
- <Provider store={store}>
- <Router history={locationService.getHistory()}>
- <Receivers />
- </Router>
- </Provider>
- );
- };
- const dataSources = {
- alertManager: mockDataSource({
- name: 'CloudManager',
- type: DataSourceType.Alertmanager,
- }),
- promAlertManager: mockDataSource<AlertManagerDataSourceJsonData>({
- 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<void> => {
- 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<HTMLTableRowElement>('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<HTMLTableRowElement>('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<HTMLTableRowElement>('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');
- });
- });
|