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