Receivers.test.tsx 19 KB

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