1import React from 'react'; 2import { locationService, setDataSourceSrv } from '@grafana/runtime'; 3import { render, waitFor } from '@testing-library/react'; 4import { Provider } from 'react-redux'; 5import { Router } from 'react-router-dom'; 6import { 7 AlertManagerCortexConfig, 8 AlertManagerDataSourceJsonData, 9 AlertManagerImplementation, 10 Route, 11} from 'app/plugins/datasource/alertmanager/types'; 12import { configureStore } from 'app/store/configureStore'; 13import { typeAsJestMock } from 'test/helpers/typeAsJestMock'; 14import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector'; 15import AmRoutes from './AmRoutes'; 16import { fetchAlertManagerConfig, fetchStatus, updateAlertManagerConfig } from './api/alertmanager'; 17import { mockDataSource, MockDataSourceSrv, someCloudAlertManagerConfig, someCloudAlertManagerStatus } from './mocks'; 18import { getAllDataSources } from './utils/config'; 19import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; 20import userEvent from '@testing-library/user-event'; 21import { selectOptionInTest } from '@grafana/ui'; 22import { ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants'; 23 24jest.mock('./api/alertmanager'); 25jest.mock('./utils/config'); 26 27const mocks = { 28 getAllDataSourcesMock: typeAsJestMock(getAllDataSources), 29 30 api: { 31 fetchAlertManagerConfig: typeAsJestMock(fetchAlertManagerConfig), 32 updateAlertManagerConfig: typeAsJestMock(updateAlertManagerConfig), 33 fetchStatus: typeAsJestMock(fetchStatus), 34 }, 35}; 36 37const renderAmRoutes = (alertManagerSourceName?: string) => { 38 const store = configureStore(); 39 locationService.push(location); 40 41 locationService.push( 42 '/alerting/routes' + (alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : '') 43 ); 44 45 return render( 46 <Provider store={store}> 47 <Router history={locationService.getHistory()}> 48 <AmRoutes /> 49 </Router> 50 </Provider> 51 ); 52}; 53 54const dataSources = { 55 am: mockDataSource({ 56 name: 'Alertmanager', 57 type: DataSourceType.Alertmanager, 58 }), 59 promAlertManager: mockDataSource<AlertManagerDataSourceJsonData>({ 60 name: 'PromManager', 61 type: DataSourceType.Alertmanager, 62 jsonData: { 63 implementation: AlertManagerImplementation.prometheus, 64 }, 65 }), 66}; 67 68const ui = { 69 rootReceiver: byTestId('am-routes-root-receiver'), 70 rootGroupBy: byTestId('am-routes-root-group-by'), 71 rootTimings: byTestId('am-routes-root-timings'), 72 row: byTestId('am-routes-row'), 73 74 rootRouteContainer: byTestId('am-root-route-container'), 75 76 editButton: byRole('button', { name: 'Edit' }), 77 saveButton: byRole('button', { name: 'Save' }), 78 79 editRouteButton: byLabelText('Edit route'), 80 deleteRouteButton: byLabelText('Delete route'), 81 newPolicyButton: byRole('button', { name: /New policy/ }), 82 newPolicyCTAButton: byRole('button', { name: /New specific policy/ }), 83 84 receiverSelect: byTestId('am-receiver-select'), 85 groupSelect: byTestId('am-group-select'), 86 87 groupWaitContainer: byTestId('am-group-wait'), 88 groupIntervalContainer: byTestId('am-group-interval'), 89 groupRepeatContainer: byTestId('am-repeat-interval'), 90}; 91 92describe('AmRoutes', () => { 93 const subroutes: Route[] = [ 94 { 95 match: { 96 sub1matcher1: 'sub1value1', 97 sub1matcher2: 'sub1value2', 98 }, 99 match_re: { 100 sub1matcher3: 'sub1value3', 101 sub1matcher4: 'sub1value4', 102 }, 103 group_by: ['sub1group1', 'sub1group2'], 104 receiver: 'a-receiver', 105 continue: true, 106 group_wait: '3s', 107 group_interval: '2m', 108 repeat_interval: '1s', 109 routes: [ 110 { 111 match: { 112 sub1sub1matcher1: 'sub1sub1value1', 113 sub1sub1matcher2: 'sub1sub1value2', 114 }, 115 match_re: { 116 sub1sub1matcher3: 'sub1sub1value3', 117 sub1sub1matcher4: 'sub1sub1value4', 118 }, 119 group_by: ['sub1sub1group1', 'sub1sub1group2'], 120 receiver: 'another-receiver', 121 }, 122 { 123 match: { 124 sub1sub2matcher1: 'sub1sub2value1', 125 sub1sub2matcher2: 'sub1sub2value2', 126 }, 127 match_re: { 128 sub1sub2matcher3: 'sub1sub2value3', 129 sub1sub2matcher4: 'sub1sub2value4', 130 }, 131 group_by: ['sub1sub2group1', 'sub1sub2group2'], 132 receiver: 'another-receiver', 133 }, 134 ], 135 }, 136 { 137 match: { 138 sub2matcher1: 'sub2value1', 139 sub2matcher2: 'sub2value2', 140 }, 141 match_re: { 142 sub2matcher3: 'sub2value3', 143 sub2matcher4: 'sub2value4', 144 }, 145 receiver: 'another-receiver', 146 }, 147 ]; 148 149 const simpleRoute: Route = { 150 receiver: 'simple-receiver', 151 matchers: ['hello=world', 'foo!=bar'], 152 }; 153 154 const rootRoute: Route = { 155 receiver: 'default-receiver', 156 group_by: ['a-group', 'another-group'], 157 group_wait: '1s', 158 group_interval: '2m', 159 repeat_interval: '3d', 160 routes: subroutes, 161 }; 162 163 beforeEach(() => { 164 mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources)); 165 setDataSourceSrv(new MockDataSourceSrv(dataSources)); 166 }); 167 168 afterEach(() => { 169 jest.resetAllMocks(); 170 171 setDataSourceSrv(undefined as any); 172 }); 173 174 it('loads and shows routes', async () => { 175 mocks.api.fetchAlertManagerConfig.mockResolvedValue({ 176 alertmanager_config: { 177 route: rootRoute, 178 receivers: [ 179 { 180 name: 'default-receiver', 181 }, 182 { 183 name: 'a-receiver', 184 }, 185 { 186 name: 'another-receiver', 187 }, 188 ], 189 }, 190 template_files: {}, 191 }); 192 193 await renderAmRoutes(); 194 195 await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1)); 196 197 expect(ui.rootReceiver.get()).toHaveTextContent(rootRoute.receiver!); 198 expect(ui.rootGroupBy.get()).toHaveTextContent(rootRoute.group_by!.join(', ')); 199 const rootTimings = ui.rootTimings.get(); 200 expect(rootTimings).toHaveTextContent(rootRoute.group_wait!); 201 expect(rootTimings).toHaveTextContent(rootRoute.group_interval!); 202 expect(rootTimings).toHaveTextContent(rootRoute.repeat_interval!); 203 204 const rows = await ui.row.findAll(); 205 expect(rows).toHaveLength(2); 206 207 subroutes.forEach((route, index) => { 208 Object.entries(route.match ?? {}).forEach(([label, value]) => { 209 expect(rows[index]).toHaveTextContent(`${label}=${value}`); 210 }); 211 212 Object.entries(route.match_re ?? {}).forEach(([label, value]) => { 213 expect(rows[index]).toHaveTextContent(`${label}=~${value}`); 214 }); 215 216 if (route.group_by) { 217 expect(rows[index]).toHaveTextContent(route.group_by.join(', ')); 218 } 219 220 if (route.receiver) { 221 expect(rows[index]).toHaveTextContent(route.receiver); 222 } 223 }); 224 }); 225 226 it('can edit root route if one is already defined', async () => { 227 const defaultConfig: AlertManagerCortexConfig = { 228 alertmanager_config: { 229 receivers: [{ name: 'default' }, { name: 'critical' }], 230 route: { 231 receiver: 'default', 232 group_by: ['alertname'], 233 }, 234 templates: [], 235 }, 236 template_files: {}, 237 }; 238 const currentConfig = { current: defaultConfig }; 239 mocks.api.updateAlertManagerConfig.mockImplementation((amSourceName, newConfig) => { 240 currentConfig.current = newConfig; 241 return Promise.resolve(); 242 }); 243 244 mocks.api.fetchAlertManagerConfig.mockImplementation(() => { 245 return Promise.resolve(currentConfig.current); 246 }); 247 248 await renderAmRoutes(); 249 expect(await ui.rootReceiver.find()).toHaveTextContent('default'); 250 expect(ui.rootGroupBy.get()).toHaveTextContent('alertname'); 251 252 // open root route for editing 253 const rootRouteContainer = await ui.rootRouteContainer.find(); 254 userEvent.click(ui.editButton.get(rootRouteContainer)); 255 256 // configure receiver & group by 257 const receiverSelect = await ui.receiverSelect.find(); 258 await clickSelectOption(receiverSelect, 'critical'); 259 260 const groupSelect = ui.groupSelect.get(); 261 userEvent.type(byRole('textbox').get(groupSelect), 'namespace{enter}'); 262 263 // configure timing intervals 264 userEvent.click(byText('Timing options').get(rootRouteContainer)); 265 266 await updateTiming(ui.groupWaitContainer.get(), '1', 'Minutes'); 267 await updateTiming(ui.groupIntervalContainer.get(), '4', 'Minutes'); 268 await updateTiming(ui.groupRepeatContainer.get(), '5', 'Hours'); 269 270 //save 271 userEvent.click(ui.saveButton.get(rootRouteContainer)); 272 273 // wait for it to go out of edit mode 274 await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument()); 275 276 // check that appropriate api calls were made 277 expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(3); 278 expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledTimes(1); 279 expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, { 280 alertmanager_config: { 281 receivers: [{ name: 'default' }, { name: 'critical' }], 282 route: { 283 continue: false, 284 group_by: ['alertname', 'namespace'], 285 receiver: 'critical', 286 routes: [], 287 group_interval: '4m', 288 group_wait: '1m', 289 repeat_interval: '5h', 290 }, 291 templates: [], 292 }, 293 template_files: {}, 294 }); 295 296 // check that new config values are rendered 297 await waitFor(() => expect(ui.rootReceiver.query()).toHaveTextContent('critical')); 298 expect(ui.rootGroupBy.get()).toHaveTextContent('alertname, namespace'); 299 }); 300 301 it('can edit root route if one is not defined yet', async () => { 302 mocks.api.fetchAlertManagerConfig.mockResolvedValue({ 303 alertmanager_config: { 304 receivers: [{ name: 'default' }], 305 }, 306 template_files: {}, 307 }); 308 309 await renderAmRoutes(); 310 311 // open root route for editing 312 const rootRouteContainer = await ui.rootRouteContainer.find(); 313 userEvent.click(ui.editButton.get(rootRouteContainer)); 314 315 // configure receiver & group by 316 const receiverSelect = await ui.receiverSelect.find(); 317 await clickSelectOption(receiverSelect, 'default'); 318 319 const groupSelect = ui.groupSelect.get(); 320 userEvent.type(byRole('textbox').get(groupSelect), 'severity{enter}'); 321 userEvent.type(byRole('textbox').get(groupSelect), 'namespace{enter}'); 322 //save 323 userEvent.click(ui.saveButton.get(rootRouteContainer)); 324 325 // wait for it to go out of edit mode 326 await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument()); 327 328 // check that appropriate api calls were made 329 expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(3); 330 expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledTimes(1); 331 expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, { 332 alertmanager_config: { 333 receivers: [{ name: 'default' }], 334 route: { 335 continue: false, 336 group_by: ['severity', 'namespace'], 337 receiver: 'default', 338 routes: [], 339 }, 340 }, 341 template_files: {}, 342 }); 343 }); 344 345 it('Show error message if loading Alertmanager config fails', async () => { 346 mocks.api.fetchAlertManagerConfig.mockRejectedValue({ 347 status: 500, 348 data: { 349 message: "Alertmanager has exploded. it's gone. Forget about it.", 350 }, 351 }); 352 await renderAmRoutes(); 353 await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1)); 354 expect(await byText("Alertmanager has exploded. it's gone. Forget about it.").find()).toBeInTheDocument(); 355 expect(ui.rootReceiver.query()).not.toBeInTheDocument(); 356 expect(ui.editButton.query()).not.toBeInTheDocument(); 357 }); 358 359 it('Converts matchers to object_matchers for grafana alertmanager', async () => { 360 const defaultConfig: AlertManagerCortexConfig = { 361 alertmanager_config: { 362 receivers: [{ name: 'default' }, { name: 'critical' }], 363 route: { 364 continue: false, 365 receiver: 'default', 366 group_by: ['alertname'], 367 routes: [simpleRoute], 368 group_interval: '4m', 369 group_wait: '1m', 370 repeat_interval: '5h', 371 }, 372 templates: [], 373 }, 374 template_files: {}, 375 }; 376 377 const currentConfig = { current: defaultConfig }; 378 mocks.api.updateAlertManagerConfig.mockImplementation((amSourceName, newConfig) => { 379 currentConfig.current = newConfig; 380 return Promise.resolve(); 381 }); 382 383 mocks.api.fetchAlertManagerConfig.mockImplementation(() => { 384 return Promise.resolve(currentConfig.current); 385 }); 386 387 await renderAmRoutes(GRAFANA_RULES_SOURCE_NAME); 388 expect(await ui.rootReceiver.find()).toHaveTextContent('default'); 389 expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled(); 390 391 // Toggle a save to test new object_matchers 392 const rootRouteContainer = await ui.rootRouteContainer.find(); 393 userEvent.click(ui.editButton.get(rootRouteContainer)); 394 userEvent.click(ui.saveButton.get(rootRouteContainer)); 395 396 await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument()); 397 398 expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled(); 399 expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, { 400 alertmanager_config: { 401 receivers: [{ name: 'default' }, { name: 'critical' }], 402 route: { 403 continue: false, 404 group_by: ['alertname'], 405 group_interval: '4m', 406 group_wait: '1m', 407 receiver: 'default', 408 repeat_interval: '5h', 409 routes: [ 410 { 411 continue: false, 412 group_by: [], 413 object_matchers: [ 414 ['hello', '=', 'world'], 415 ['foo', '!=', 'bar'], 416 ], 417 receiver: 'simple-receiver', 418 routes: [], 419 }, 420 ], 421 }, 422 templates: [], 423 }, 424 template_files: {}, 425 }); 426 }); 427 428 it('Keeps matchers for non-grafana alertmanager sources', async () => { 429 const defaultConfig: AlertManagerCortexConfig = { 430 alertmanager_config: { 431 receivers: [{ name: 'default' }, { name: 'critical' }], 432 route: { 433 continue: false, 434 receiver: 'default', 435 group_by: ['alertname'], 436 routes: [simpleRoute], 437 group_interval: '4m', 438 group_wait: '1m', 439 repeat_interval: '5h', 440 }, 441 templates: [], 442 }, 443 template_files: {}, 444 }; 445 446 const currentConfig = { current: defaultConfig }; 447 mocks.api.updateAlertManagerConfig.mockImplementation((amSourceName, newConfig) => { 448 currentConfig.current = newConfig; 449 return Promise.resolve(); 450 }); 451 452 mocks.api.fetchAlertManagerConfig.mockImplementation(() => { 453 return Promise.resolve(currentConfig.current); 454 }); 455 456 await renderAmRoutes(dataSources.am.name); 457 expect(await ui.rootReceiver.find()).toHaveTextContent('default'); 458 expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled(); 459 460 // Toggle a save to test new object_matchers 461 const rootRouteContainer = await ui.rootRouteContainer.find(); 462 userEvent.click(ui.editButton.get(rootRouteContainer)); 463 userEvent.click(ui.saveButton.get(rootRouteContainer)); 464 465 await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument()); 466 467 expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled(); 468 expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(dataSources.am.name, { 469 alertmanager_config: { 470 receivers: [{ name: 'default' }, { name: 'critical' }], 471 route: { 472 continue: false, 473 group_by: ['alertname'], 474 group_interval: '4m', 475 group_wait: '1m', 476 matchers: [], 477 receiver: 'default', 478 repeat_interval: '5h', 479 routes: [ 480 { 481 continue: false, 482 group_by: [], 483 matchers: ['hello=world', 'foo!=bar'], 484 receiver: 'simple-receiver', 485 routes: [], 486 }, 487 ], 488 }, 489 templates: [], 490 }, 491 template_files: {}, 492 }); 493 }); 494 495 it('Prometheus Alertmanager routes cannot be edited', async () => { 496 mocks.api.fetchStatus.mockResolvedValue({ 497 ...someCloudAlertManagerStatus, 498 config: someCloudAlertManagerConfig.alertmanager_config, 499 }); 500 await renderAmRoutes(dataSources.promAlertManager.name); 501 const rootRouteContainer = await ui.rootRouteContainer.find(); 502 expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument(); 503 const rows = await ui.row.findAll(); 504 expect(rows).toHaveLength(2); 505 expect(ui.editRouteButton.query()).not.toBeInTheDocument(); 506 expect(ui.deleteRouteButton.query()).not.toBeInTheDocument(); 507 expect(ui.saveButton.query()).not.toBeInTheDocument(); 508 509 expect(mocks.api.fetchAlertManagerConfig).not.toHaveBeenCalled(); 510 expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1); 511 }); 512 513 it('Prometheus Alertmanager has no CTA button if there are no specific policies', async () => { 514 mocks.api.fetchStatus.mockResolvedValue({ 515 ...someCloudAlertManagerStatus, 516 config: { 517 ...someCloudAlertManagerConfig.alertmanager_config, 518 route: { 519 ...someCloudAlertManagerConfig.alertmanager_config.route, 520 routes: undefined, 521 }, 522 }, 523 }); 524 await renderAmRoutes(dataSources.promAlertManager.name); 525 const rootRouteContainer = await ui.rootRouteContainer.find(); 526 expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument(); 527 expect(ui.newPolicyCTAButton.query()).not.toBeInTheDocument(); 528 expect(mocks.api.fetchAlertManagerConfig).not.toHaveBeenCalled(); 529 expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1); 530 }); 531}); 532 533const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise<void> => { 534 userEvent.click(byRole('textbox').get(selectElement)); 535 await selectOptionInTest(selectElement, optionText); 536}; 537 538const updateTiming = async (selectElement: HTMLElement, value: string, timeUnit: string): Promise<void> => { 539 const inputs = byRole('textbox').queryAll(selectElement); 540 expect(inputs).toHaveLength(2); 541 userEvent.type(inputs[0], value); 542 userEvent.click(inputs[1]); 543 await selectOptionInTest(selectElement, timeUnit); 544}; 545