1import { configureStore } from 'app/store/configureStore';
2import { Provider } from 'react-redux';
3import { Router } from 'react-router-dom';
4import Receivers from './Receivers';
5import React from 'react';
6import { locationService, setDataSourceSrv } from '@grafana/runtime';
7import { act, render, waitFor } from '@testing-library/react';
8import { getAllDataSources } from './utils/config';
9import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
10import { updateAlertManagerConfig, fetchAlertManagerConfig, fetchStatus, testReceivers } from './api/alertmanager';
11import {
12  mockDataSource,
13  MockDataSourceSrv,
14  someCloudAlertManagerConfig,
15  someCloudAlertManagerStatus,
16  someGrafanaAlertManagerConfig,
17} from './mocks';
18import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
19import { fetchNotifiers } from './api/grafana';
20import { grafanaNotifiersMock } from './mocks/grafana-notifiers';
21import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector';
22import userEvent from '@testing-library/user-event';
23import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
24import store from 'app/core/store';
25import { contextSrv } from 'app/core/services/context_srv';
26import { selectOptionInTest } from '@grafana/ui';
27import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
28
29jest.mock('./api/alertmanager');
30jest.mock('./api/grafana');
31jest.mock('./utils/config');
32
33const mocks = {
34  getAllDataSources: typeAsJestMock(getAllDataSources),
35
36  api: {
37    fetchConfig: typeAsJestMock(fetchAlertManagerConfig),
38    fetchStatus: typeAsJestMock(fetchStatus),
39    updateConfig: typeAsJestMock(updateAlertManagerConfig),
40    fetchNotifiers: typeAsJestMock(fetchNotifiers),
41    testReceivers: typeAsJestMock(testReceivers),
42  },
43};
44
45const renderReceivers = (alertManagerSourceName?: string) => {
46  const store = configureStore();
47
48  locationService.push(
49    '/alerting/notifications' +
50      (alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : '')
51  );
52
53  return render(
54    <Provider store={store}>
55      <Router history={locationService.getHistory()}>
56        <Receivers />
57      </Router>
58    </Provider>
59  );
60};
61
62const dataSources = {
63  alertManager: mockDataSource({
64    name: 'CloudManager',
65    type: DataSourceType.Alertmanager,
66  }),
67  promAlertManager: mockDataSource<AlertManagerDataSourceJsonData>({
68    name: 'PromManager',
69    type: DataSourceType.Alertmanager,
70    jsonData: {
71      implementation: AlertManagerImplementation.prometheus,
72    },
73  }),
74};
75
76const ui = {
77  newContactPointButton: byRole('link', { name: /new contact point/i }),
78  saveContactButton: byRole('button', { name: /save contact point/i }),
79  newContactPointTypeButton: byRole('button', { name: /new contact point type/i }),
80  testContactPointButton: byRole('button', { name: /Test/ }),
81  testContactPointModal: byRole('heading', { name: /test contact point/i }),
82  customContactPointOption: byRole('radio', { name: /custom/i }),
83  contactPointAnnotationSelect: (idx: number) => byTestId(`annotation-key-${idx}`),
84  contactPointAnnotationValue: (idx: number) => byTestId(`annotation-value-${idx}`),
85  contactPointLabelKey: (idx: number) => byTestId(`label-key-${idx}`),
86  contactPointLabelValue: (idx: number) => byTestId(`label-value-${idx}`),
87  testContactPoint: byRole('button', { name: /send test notification/i }),
88  cancelButton: byTestId('cancel-button'),
89
90  receiversTable: byTestId('receivers-table'),
91  templatesTable: byTestId('templates-table'),
92  alertManagerPicker: byTestId('alertmanager-picker'),
93
94  channelFormContainer: byTestId('item-container'),
95
96  inputs: {
97    name: byPlaceholderText('Name'),
98    email: {
99      addresses: byLabelText(/Addresses/),
100      toEmails: byLabelText(/To/),
101    },
102    hipchat: {
103      url: byLabelText('Hip Chat Url'),
104      apiKey: byLabelText('API Key'),
105    },
106    slack: {
107      webhookURL: byLabelText(/Webhook URL/i),
108    },
109    webhook: {
110      URL: byLabelText(/The endpoint to send HTTP POST requests to/i),
111    },
112  },
113};
114
115const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise<void> => {
116  userEvent.click(byRole('textbox').get(selectElement));
117  await selectOptionInTest(selectElement, optionText);
118};
119
120describe('Receivers', () => {
121  beforeEach(() => {
122    jest.resetAllMocks();
123    mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
124    mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock);
125    setDataSourceSrv(new MockDataSourceSrv(dataSources));
126    contextSrv.isEditor = true;
127    store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
128  });
129
130  it('Template and receiver tables are rendered, alertmanager can be selected', async () => {
131    mocks.api.fetchConfig.mockImplementation((name) =>
132      Promise.resolve(name === GRAFANA_RULES_SOURCE_NAME ? someGrafanaAlertManagerConfig : someCloudAlertManagerConfig)
133    );
134    await renderReceivers();
135
136    // check that by default grafana templates & receivers are fetched rendered in appropriate tables
137    let receiversTable = await ui.receiversTable.find();
138    let templatesTable = await ui.templatesTable.find();
139    let templateRows = templatesTable.querySelectorAll('tbody tr');
140    expect(templateRows).toHaveLength(3);
141    expect(templateRows[0]).toHaveTextContent('first template');
142    expect(templateRows[1]).toHaveTextContent('second template');
143    expect(templateRows[2]).toHaveTextContent('third template');
144    let receiverRows = receiversTable.querySelectorAll('tbody tr');
145    expect(receiverRows[0]).toHaveTextContent('default');
146    expect(receiverRows[1]).toHaveTextContent('critical');
147    expect(receiverRows).toHaveLength(2);
148
149    expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1);
150    expect(mocks.api.fetchConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME);
151    expect(mocks.api.fetchNotifiers).toHaveBeenCalledTimes(1);
152    expect(locationService.getSearchObject()[ALERTMANAGER_NAME_QUERY_KEY]).toEqual(undefined);
153
154    // select external cloud alertmanager, check that data is retrieved and contents are rendered as appropriate
155    await clickSelectOption(ui.alertManagerPicker.get(), 'CloudManager');
156    await byText('cloud-receiver').find();
157    expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(2);
158    expect(mocks.api.fetchConfig).toHaveBeenLastCalledWith('CloudManager');
159
160    receiversTable = await ui.receiversTable.find();
161    templatesTable = await ui.templatesTable.find();
162    templateRows = templatesTable.querySelectorAll('tbody tr');
163    expect(templateRows[0]).toHaveTextContent('foo template');
164    expect(templateRows).toHaveLength(1);
165    receiverRows = receiversTable.querySelectorAll('tbody tr');
166    expect(receiverRows[0]).toHaveTextContent('cloud-receiver');
167    expect(receiverRows).toHaveLength(1);
168    expect(locationService.getSearchObject()[ALERTMANAGER_NAME_QUERY_KEY]).toEqual('CloudManager');
169  });
170
171  it('Grafana receiver can be tested', async () => {
172    mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
173
174    await renderReceivers();
175
176    // go to new contact point page
177    userEvent.click(await ui.newContactPointButton.find());
178
179    await byRole('heading', { name: /create contact point/i }).find();
180    expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/new');
181
182    // type in a name for the new receiver
183    userEvent.type(ui.inputs.name.get(), 'my new receiver');
184
185    // enter some email
186    const email = ui.inputs.email.addresses.get();
187    userEvent.clear(email);
188    userEvent.type(email, 'tester@grafana.com');
189
190    // try to test the contact point
191    userEvent.click(ui.testContactPointButton.get());
192
193    await waitFor(() => expect(ui.testContactPointModal.get()).toBeInTheDocument());
194    userEvent.click(ui.customContactPointOption.get());
195    await waitFor(() => expect(ui.contactPointAnnotationSelect(0).get()).toBeInTheDocument());
196
197    // enter custom annotations and labels
198    await clickSelectOption(ui.contactPointAnnotationSelect(0).get(), 'Description');
199    await userEvent.type(ui.contactPointAnnotationValue(0).get(), 'Test contact point');
200    await userEvent.type(ui.contactPointLabelKey(0).get(), 'foo');
201    await userEvent.type(ui.contactPointLabelValue(0).get(), 'bar');
202    userEvent.click(ui.testContactPoint.get());
203
204    await waitFor(() => expect(mocks.api.testReceivers).toHaveBeenCalled());
205
206    expect(mocks.api.testReceivers).toHaveBeenCalledWith(
207      'grafana',
208      [
209        {
210          grafana_managed_receiver_configs: [
211            {
212              disableResolveMessage: false,
213              name: 'test',
214              secureSettings: {},
215              settings: { addresses: 'tester@grafana.com', singleEmail: false },
216              type: 'email',
217            },
218          ],
219          name: 'test',
220        },
221      ],
222      { annotations: { description: 'Test contact point' }, labels: { foo: 'bar' } }
223    );
224  });
225
226  it('Grafana receiver can be created', async () => {
227    mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
228    mocks.api.updateConfig.mockResolvedValue();
229    await renderReceivers();
230
231    // go to new contact point page
232    await userEvent.click(await ui.newContactPointButton.find());
233
234    await byRole('heading', { name: /create contact point/i }).find();
235    expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/new');
236
237    // type in a name for the new receiver
238    userEvent.type(byPlaceholderText('Name').get(), 'my new receiver');
239
240    // check that default email form is rendered
241    await ui.inputs.email.addresses.find();
242
243    // select hipchat
244    await clickSelectOption(byTestId('items.0.type').get(), 'HipChat');
245
246    // check that email options are gone and hipchat options appear
247    expect(ui.inputs.email.addresses.query()).not.toBeInTheDocument();
248
249    const urlInput = ui.inputs.hipchat.url.get();
250    const apiKeyInput = ui.inputs.hipchat.apiKey.get();
251
252    userEvent.type(urlInput, 'http://hipchat');
253    userEvent.type(apiKeyInput, 'foobarbaz');
254
255    // it seems react-hook-form does some async state updates after submit
256    await act(async () => {
257      await userEvent.click(ui.saveContactButton.get());
258    });
259
260    // see that we're back to main page and proper api calls have been made
261    await ui.receiversTable.find();
262    expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1);
263    expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3);
264    expect(locationService.getLocation().pathname).toEqual('/alerting/notifications');
265    expect(mocks.api.updateConfig).toHaveBeenLastCalledWith(GRAFANA_RULES_SOURCE_NAME, {
266      ...someGrafanaAlertManagerConfig,
267      alertmanager_config: {
268        ...someGrafanaAlertManagerConfig.alertmanager_config,
269        receivers: [
270          ...(someGrafanaAlertManagerConfig.alertmanager_config.receivers ?? []),
271          {
272            name: 'my new receiver',
273            grafana_managed_receiver_configs: [
274              {
275                disableResolveMessage: false,
276                name: 'my new receiver',
277                secureSettings: {},
278                settings: {
279                  apiKey: 'foobarbaz',
280                  url: 'http://hipchat',
281                },
282                type: 'hipchat',
283              },
284            ],
285          },
286        ],
287      },
288    });
289  });
290
291  it('Cloud alertmanager receiver can be edited', async () => {
292    mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig);
293    mocks.api.updateConfig.mockResolvedValue();
294    await renderReceivers('CloudManager');
295
296    // click edit button for the receiver
297    const receiversTable = await ui.receiversTable.find();
298    const receiverRows = receiversTable.querySelectorAll<HTMLTableRowElement>('tbody tr');
299    expect(receiverRows[0]).toHaveTextContent('cloud-receiver');
300    await userEvent.click(byTestId('edit').get(receiverRows[0]));
301
302    // check that form is open
303    await byRole('heading', { name: /update contact point/i }).find();
304    expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/cloud-receiver/edit');
305    expect(ui.channelFormContainer.queryAll()).toHaveLength(2);
306
307    // delete the email channel
308    expect(ui.channelFormContainer.queryAll()).toHaveLength(2);
309    await userEvent.click(byTestId('items.0.delete-button').get());
310    expect(ui.channelFormContainer.queryAll()).toHaveLength(1);
311
312    // modify webhook url
313    const slackContainer = ui.channelFormContainer.get();
314    await userEvent.click(byText('Optional Slack settings').get(slackContainer));
315    userEvent.type(ui.inputs.slack.webhookURL.get(slackContainer), 'http://newgreaturl');
316
317    // add confirm button to action
318    await userEvent.click(byText(/Actions \(1\)/i).get(slackContainer));
319    await userEvent.click(await byTestId('items.1.settings.actions.0.confirm.add-button').find());
320    const confirmSubform = byTestId('items.1.settings.actions.0.confirm.container').get();
321    userEvent.type(byLabelText('Text').get(confirmSubform), 'confirm this');
322
323    // delete a field
324    await userEvent.click(byText(/Fields \(2\)/i).get(slackContainer));
325    await userEvent.click(byTestId('items.1.settings.fields.0.delete-button').get());
326    await byText(/Fields \(1\)/i).get(slackContainer);
327
328    // add another channel
329    await userEvent.click(ui.newContactPointTypeButton.get());
330    await clickSelectOption(await byTestId('items.2.type').find(), 'Webhook');
331    userEvent.type(await ui.inputs.webhook.URL.find(), 'http://webhookurl');
332
333    // it seems react-hook-form does some async state updates after submit
334    await act(async () => {
335      await userEvent.click(ui.saveContactButton.get());
336    });
337
338    // see that we're back to main page and proper api calls have been made
339    await ui.receiversTable.find();
340    expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1);
341    expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3);
342    expect(locationService.getLocation().pathname).toEqual('/alerting/notifications');
343    expect(mocks.api.updateConfig).toHaveBeenLastCalledWith('CloudManager', {
344      ...someCloudAlertManagerConfig,
345      alertmanager_config: {
346        ...someCloudAlertManagerConfig.alertmanager_config,
347        receivers: [
348          {
349            name: 'cloud-receiver',
350            slack_configs: [
351              {
352                actions: [
353                  {
354                    confirm: {
355                      text: 'confirm this',
356                    },
357                    text: 'action1text',
358                    type: 'action1type',
359                    url: 'http://action1',
360                  },
361                ],
362                api_url: 'http://slack1http://newgreaturl',
363                channel: '#mychannel',
364                fields: [
365                  {
366                    short: false,
367                    title: 'field2',
368                    value: 'text2',
369                  },
370                ],
371                link_names: false,
372                send_resolved: false,
373                short_fields: false,
374              },
375            ],
376            webhook_configs: [
377              {
378                send_resolved: true,
379                url: 'http://webhookurl',
380              },
381            ],
382          },
383        ],
384      },
385    });
386  });
387
388  it('Prometheus Alertmanager receiver cannot be edited', async () => {
389    mocks.api.fetchStatus.mockResolvedValue({
390      ...someCloudAlertManagerStatus,
391      config: someCloudAlertManagerConfig.alertmanager_config,
392    });
393    await renderReceivers(dataSources.promAlertManager.name);
394
395    const receiversTable = await ui.receiversTable.find();
396    // there's no templates table for vanilla prom, API does not return templates
397    expect(ui.templatesTable.query()).not.toBeInTheDocument();
398
399    // click view button on the receiver
400    const receiverRows = receiversTable.querySelectorAll<HTMLTableRowElement>('tbody tr');
401    expect(receiverRows[0]).toHaveTextContent('cloud-receiver');
402    expect(byTestId('edit').query(receiverRows[0])).not.toBeInTheDocument();
403    await userEvent.click(byTestId('view').get(receiverRows[0]));
404
405    // check that form is open
406    await byRole('heading', { name: /contact point/i }).find();
407    expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/cloud-receiver/edit');
408    const channelForms = ui.channelFormContainer.queryAll();
409    expect(channelForms).toHaveLength(2);
410
411    // check that inputs are disabled and there is no save button
412    expect(ui.inputs.name.queryAll()[0]).toHaveAttribute('readonly');
413    expect(ui.inputs.email.toEmails.get(channelForms[0])).toHaveAttribute('readonly');
414    expect(ui.inputs.slack.webhookURL.get(channelForms[1])).toHaveAttribute('readonly');
415    expect(ui.newContactPointButton.query()).not.toBeInTheDocument();
416    expect(ui.testContactPointButton.query()).not.toBeInTheDocument();
417    expect(ui.saveContactButton.query()).not.toBeInTheDocument();
418    expect(ui.cancelButton.query()).toBeInTheDocument();
419
420    expect(mocks.api.fetchConfig).not.toHaveBeenCalled();
421    expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1);
422  });
423
424  it('Loads config from status endpoint if there is no user config', async () => {
425    // loading an empty config with make it fetch config from status endpoint
426    mocks.api.fetchConfig.mockResolvedValue({
427      template_files: {},
428      alertmanager_config: {},
429    });
430    mocks.api.fetchStatus.mockResolvedValue(someCloudAlertManagerStatus);
431    await renderReceivers('CloudManager');
432
433    // check that receiver from the default config is represented
434    const receiversTable = await ui.receiversTable.find();
435    const receiverRows = receiversTable.querySelectorAll<HTMLTableRowElement>('tbody tr');
436    expect(receiverRows[0]).toHaveTextContent('default-email');
437
438    // check that both config and status endpoints were called
439    expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1);
440    expect(mocks.api.fetchConfig).toHaveBeenLastCalledWith('CloudManager');
441    expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1);
442    expect(mocks.api.fetchStatus).toHaveBeenLastCalledWith('CloudManager');
443  });
444});
445