Silences.test.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. import { render, waitFor } from '@testing-library/react';
  2. import userEvent, { PointerEventsCheckLevel } 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 { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector';
  7. import { dateTime } from '@grafana/data';
  8. import { locationService, setDataSourceSrv } from '@grafana/runtime';
  9. import { contextSrv } from 'app/core/services/context_srv';
  10. import { AlertState, MatcherOperator } from 'app/plugins/datasource/alertmanager/types';
  11. import { configureStore } from 'app/store/configureStore';
  12. import { AccessControlAction } from 'app/types';
  13. import Silences from './Silences';
  14. import { fetchSilences, fetchAlerts, createOrUpdateSilence } from './api/alertmanager';
  15. import { mockAlertmanagerAlert, mockDataSource, MockDataSourceSrv, mockSilence } from './mocks';
  16. import { parseMatchers } from './utils/alertmanager';
  17. import { DataSourceType } from './utils/datasource';
  18. jest.mock('./api/alertmanager');
  19. jest.mock('app/core/services/context_srv');
  20. const TEST_TIMEOUT = 60000;
  21. const mocks = {
  22. api: {
  23. fetchSilences: jest.mocked(fetchSilences),
  24. fetchAlerts: jest.mocked(fetchAlerts),
  25. createOrUpdateSilence: jest.mocked(createOrUpdateSilence),
  26. },
  27. contextSrv: jest.mocked(contextSrv),
  28. };
  29. const renderSilences = (location = '/alerting/silences/') => {
  30. const store = configureStore();
  31. locationService.push(location);
  32. return render(
  33. <Provider store={store}>
  34. <Router history={locationService.getHistory()}>
  35. <Silences />
  36. </Router>
  37. </Provider>
  38. );
  39. };
  40. const dataSources = {
  41. am: mockDataSource({
  42. name: 'Alertmanager',
  43. type: DataSourceType.Alertmanager,
  44. }),
  45. };
  46. const ui = {
  47. silencesTable: byTestId('dynamic-table'),
  48. silenceRow: byTestId('row'),
  49. silencedAlertCell: byTestId('alerts'),
  50. addSilenceButton: byRole('button', { name: /new silence/i }),
  51. queryBar: byPlaceholderText('Search'),
  52. editor: {
  53. timeRange: byLabelText('Timepicker', { exact: false }),
  54. durationField: byLabelText('Duration'),
  55. durationInput: byRole('textbox', { name: /duration/i }),
  56. matchersField: byTestId('matcher'),
  57. matcherName: byPlaceholderText('label'),
  58. matcherValue: byPlaceholderText('value'),
  59. comment: byPlaceholderText('Details about the silence'),
  60. matcherOperatorSelect: byLabelText('operator'),
  61. matcherOperator: (operator: MatcherOperator) => byText(operator, { exact: true }),
  62. addMatcherButton: byRole('button', { name: 'Add matcher' }),
  63. submit: byText('Submit'),
  64. },
  65. };
  66. const resetMocks = () => {
  67. jest.resetAllMocks();
  68. mocks.api.fetchSilences.mockImplementation(() => {
  69. return Promise.resolve([
  70. mockSilence({ id: '12345' }),
  71. mockSilence({ id: '67890', matchers: parseMatchers('foo!=bar'), comment: 'Catch all' }),
  72. ]);
  73. });
  74. mocks.api.fetchAlerts.mockImplementation(() => {
  75. return Promise.resolve([
  76. mockAlertmanagerAlert({
  77. labels: { foo: 'bar' },
  78. status: { state: AlertState.Suppressed, silencedBy: ['12345'], inhibitedBy: [] },
  79. }),
  80. mockAlertmanagerAlert({
  81. labels: { foo: 'buzz' },
  82. status: { state: AlertState.Suppressed, silencedBy: ['67890'], inhibitedBy: [] },
  83. }),
  84. ]);
  85. });
  86. mocks.api.createOrUpdateSilence.mockResolvedValue(mockSilence());
  87. mocks.contextSrv.evaluatePermission.mockImplementation(() => []);
  88. mocks.contextSrv.hasPermission.mockImplementation((action) => {
  89. const permissions = [
  90. AccessControlAction.AlertingInstanceRead,
  91. AccessControlAction.AlertingInstanceCreate,
  92. AccessControlAction.AlertingInstanceUpdate,
  93. AccessControlAction.AlertingInstancesExternalRead,
  94. AccessControlAction.AlertingInstancesExternalWrite,
  95. ];
  96. return permissions.includes(action as AccessControlAction);
  97. });
  98. mocks.contextSrv.hasAccess.mockImplementation(() => true);
  99. };
  100. describe('Silences', () => {
  101. beforeAll(resetMocks);
  102. afterEach(resetMocks);
  103. beforeEach(() => {
  104. setDataSourceSrv(new MockDataSourceSrv(dataSources));
  105. });
  106. it(
  107. 'loads and shows silences',
  108. async () => {
  109. renderSilences();
  110. await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled());
  111. await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled());
  112. expect(ui.silencesTable.query()).not.toBeNull();
  113. const silences = ui.silenceRow.queryAll();
  114. expect(silences).toHaveLength(2);
  115. expect(silences[0]).toHaveTextContent('foo=bar');
  116. expect(silences[1]).toHaveTextContent('foo!=bar');
  117. },
  118. TEST_TIMEOUT
  119. );
  120. it(
  121. 'shows the correct number of silenced alerts',
  122. async () => {
  123. mocks.api.fetchAlerts.mockImplementation(() => {
  124. return Promise.resolve([
  125. mockAlertmanagerAlert({
  126. labels: { foo: 'bar', buzz: 'bazz' },
  127. status: { state: AlertState.Suppressed, silencedBy: ['12345'], inhibitedBy: [] },
  128. }),
  129. mockAlertmanagerAlert({
  130. labels: { foo: 'bar', buzz: 'bazz' },
  131. status: { state: AlertState.Suppressed, silencedBy: ['12345'], inhibitedBy: [] },
  132. }),
  133. ]);
  134. });
  135. renderSilences();
  136. await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled());
  137. await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled());
  138. const silencedAlertRows = ui.silencedAlertCell.getAll(ui.silencesTable.get());
  139. expect(silencedAlertRows).toHaveLength(2);
  140. expect(silencedAlertRows[0]).toHaveTextContent('2');
  141. expect(silencedAlertRows[1]).toHaveTextContent('0');
  142. },
  143. TEST_TIMEOUT
  144. );
  145. it(
  146. 'filters silences by matchers',
  147. async () => {
  148. renderSilences();
  149. await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled());
  150. await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled());
  151. const queryBar = ui.queryBar.get();
  152. await userEvent.click(queryBar);
  153. await userEvent.paste('foo=bar');
  154. await waitFor(() => expect(ui.silenceRow.getAll()).toHaveLength(1));
  155. },
  156. TEST_TIMEOUT
  157. );
  158. it('shows creating a silence button for users with access', async () => {
  159. renderSilences();
  160. await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled());
  161. await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled());
  162. expect(ui.addSilenceButton.get()).toBeInTheDocument();
  163. });
  164. it('hides actions for creating a silence for users without access', async () => {
  165. mocks.contextSrv.hasAccess.mockImplementation((action) => {
  166. const permissions = [AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead];
  167. return permissions.includes(action as AccessControlAction);
  168. });
  169. renderSilences();
  170. await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled());
  171. await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled());
  172. expect(ui.addSilenceButton.query()).not.toBeInTheDocument();
  173. });
  174. });
  175. describe('Silence edit', () => {
  176. const baseUrlPath = '/alerting/silence/new';
  177. beforeAll(resetMocks);
  178. afterEach(resetMocks);
  179. beforeEach(() => {
  180. setDataSourceSrv(new MockDataSourceSrv(dataSources));
  181. });
  182. it(
  183. 'prefills the matchers field with matchers params',
  184. async () => {
  185. const matchersParams = ['foo=bar', 'bar=~ba.+', 'hello!=world', 'cluster!~us-central.*'];
  186. const matchersQueryString = matchersParams.map((matcher) => `matcher=${encodeURIComponent(matcher)}`).join('&');
  187. renderSilences(`${baseUrlPath}?${matchersQueryString}`);
  188. await waitFor(() => expect(ui.editor.durationField.query()).not.toBeNull());
  189. const matchers = ui.editor.matchersField.queryAll();
  190. expect(matchers).toHaveLength(4);
  191. expect(ui.editor.matcherName.query(matchers[0])).toHaveValue('foo');
  192. expect(ui.editor.matcherOperator(MatcherOperator.equal).query(matchers[0])).not.toBeNull();
  193. expect(ui.editor.matcherValue.query(matchers[0])).toHaveValue('bar');
  194. expect(ui.editor.matcherName.query(matchers[1])).toHaveValue('bar');
  195. expect(ui.editor.matcherOperator(MatcherOperator.regex).query(matchers[1])).not.toBeNull();
  196. expect(ui.editor.matcherValue.query(matchers[1])).toHaveValue('ba.+');
  197. expect(ui.editor.matcherName.query(matchers[2])).toHaveValue('hello');
  198. expect(ui.editor.matcherOperator(MatcherOperator.notEqual).query(matchers[2])).not.toBeNull();
  199. expect(ui.editor.matcherValue.query(matchers[2])).toHaveValue('world');
  200. expect(ui.editor.matcherName.query(matchers[3])).toHaveValue('cluster');
  201. expect(ui.editor.matcherOperator(MatcherOperator.notRegex).query(matchers[3])).not.toBeNull();
  202. expect(ui.editor.matcherValue.query(matchers[3])).toHaveValue('us-central.*');
  203. },
  204. TEST_TIMEOUT
  205. );
  206. it(
  207. 'creates a new silence',
  208. async () => {
  209. renderSilences(baseUrlPath);
  210. await waitFor(() => expect(ui.editor.durationField.query()).not.toBeNull());
  211. const start = new Date();
  212. const end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
  213. const now = dateTime().format('YYYY-MM-DD HH:mm');
  214. const startDateString = dateTime(start).format('YYYY-MM-DD');
  215. const endDateString = dateTime(end).format('YYYY-MM-DD');
  216. await userEvent.clear(ui.editor.durationInput.get());
  217. await userEvent.type(ui.editor.durationInput.get(), '1d');
  218. await waitFor(() => expect(ui.editor.durationInput.query()).toHaveValue('1d'));
  219. await waitFor(() => expect(ui.editor.timeRange.get()).toHaveTextContent(startDateString));
  220. await waitFor(() => expect(ui.editor.timeRange.get()).toHaveTextContent(endDateString));
  221. await userEvent.type(ui.editor.matcherName.get(), 'foo');
  222. await userEvent.type(ui.editor.matcherOperatorSelect.get(), '=');
  223. await userEvent.tab();
  224. await userEvent.type(ui.editor.matcherValue.get(), 'bar');
  225. // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
  226. await userEvent.click(ui.editor.addMatcherButton.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never });
  227. await userEvent.type(ui.editor.matcherName.getAll()[1], 'bar');
  228. await userEvent.type(ui.editor.matcherOperatorSelect.getAll()[1], '!=');
  229. await userEvent.tab();
  230. await userEvent.type(ui.editor.matcherValue.getAll()[1], 'buzz');
  231. // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
  232. await userEvent.click(ui.editor.addMatcherButton.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never });
  233. await userEvent.type(ui.editor.matcherName.getAll()[2], 'region');
  234. await userEvent.type(ui.editor.matcherOperatorSelect.getAll()[2], '=~');
  235. await userEvent.tab();
  236. await userEvent.type(ui.editor.matcherValue.getAll()[2], 'us-west-.*');
  237. // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
  238. await userEvent.click(ui.editor.addMatcherButton.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never });
  239. await userEvent.type(ui.editor.matcherName.getAll()[3], 'env');
  240. await userEvent.type(ui.editor.matcherOperatorSelect.getAll()[3], '!~');
  241. await userEvent.tab();
  242. await userEvent.type(ui.editor.matcherValue.getAll()[3], 'dev|staging');
  243. await userEvent.click(ui.editor.submit.get());
  244. await waitFor(() =>
  245. expect(mocks.api.createOrUpdateSilence).toHaveBeenCalledWith(
  246. 'grafana',
  247. expect.objectContaining({
  248. comment: `created ${now}`,
  249. matchers: [
  250. { isEqual: true, isRegex: false, name: 'foo', value: 'bar' },
  251. { isEqual: false, isRegex: false, name: 'bar', value: 'buzz' },
  252. { isEqual: true, isRegex: true, name: 'region', value: 'us-west-.*' },
  253. { isEqual: false, isRegex: true, name: 'env', value: 'dev|staging' },
  254. ],
  255. })
  256. )
  257. );
  258. },
  259. TEST_TIMEOUT
  260. );
  261. });