ruleDetails.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import {browserHistory} from 'react-router';
  2. import moment from 'moment';
  3. import {ProjectAlertRule} from 'sentry-fixture/projectAlertRule';
  4. import {initializeOrg} from 'sentry-test/initializeOrg';
  5. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  6. import ProjectsStore from 'sentry/stores/projectsStore';
  7. import AlertRuleDetails from './ruleDetails';
  8. describe('AlertRuleDetails', () => {
  9. const context = initializeOrg();
  10. const organization = context.organization;
  11. const project = TestStubs.Project();
  12. const rule = ProjectAlertRule({
  13. lastTriggered: moment().subtract(2, 'day').format(),
  14. });
  15. const member = TestStubs.Member();
  16. const createWrapper = (props: any = {}, newContext?: any, org = organization) => {
  17. const router = newContext ? newContext.router : context.router;
  18. const routerContext = newContext ? newContext.routerContext : context.routerContext;
  19. return render(
  20. <AlertRuleDetails
  21. params={{
  22. orgId: org.slug,
  23. projectId: project.slug,
  24. ruleId: rule.id,
  25. }}
  26. location={{...router.location, query: {}}}
  27. router={router}
  28. {...props}
  29. />,
  30. {context: routerContext, organization: org}
  31. );
  32. };
  33. beforeEach(() => {
  34. browserHistory.push = jest.fn();
  35. MockApiClient.addMockResponse({
  36. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/`,
  37. body: rule,
  38. match: [MockApiClient.matchQuery({expand: 'lastTriggered'})],
  39. });
  40. MockApiClient.addMockResponse({
  41. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/stats/`,
  42. body: [],
  43. });
  44. MockApiClient.addMockResponse({
  45. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/group-history/`,
  46. body: [
  47. {
  48. count: 1,
  49. group: TestStubs.Group(),
  50. lastTriggered: moment('Apr 11, 2019 1:08:59 AM UTC').format(),
  51. eventId: 'eventId',
  52. },
  53. ],
  54. headers: {
  55. Link:
  56. '<https://sentry.io/api/0/projects/org-slug/project-slug/rules/1/group-history/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1", ' +
  57. '<https://sentry.io/api/0/projects/org-slug/project-slug/rules/1/group-history/?cursor=0:100:0>; rel="next"; results="true"; cursor="0:100:0"',
  58. },
  59. });
  60. MockApiClient.addMockResponse({
  61. url: `/organizations/${organization.slug}/projects/`,
  62. body: [project],
  63. });
  64. MockApiClient.addMockResponse({
  65. url: `/organizations/${organization.slug}/users/`,
  66. body: [member],
  67. });
  68. ProjectsStore.init();
  69. ProjectsStore.loadInitialData([project]);
  70. });
  71. afterEach(() => {
  72. ProjectsStore.reset();
  73. MockApiClient.clearMockResponses();
  74. });
  75. it('displays alert rule with list of issues', async () => {
  76. createWrapper();
  77. expect(await screen.findAllByText('My alert rule')).toHaveLength(2);
  78. expect(screen.getByText('RequestError:')).toBeInTheDocument();
  79. expect(screen.getByText('Apr 11, 2019 1:08:59 AM UTC')).toBeInTheDocument();
  80. expect(screen.getByText('RequestError:')).toHaveAttribute(
  81. 'href',
  82. expect.stringMatching(
  83. RegExp(
  84. `/organizations/${organization.slug}/issues/${
  85. TestStubs.Group().id
  86. }/events/eventId.*`
  87. )
  88. )
  89. );
  90. });
  91. it('should allow paginating results', async () => {
  92. createWrapper();
  93. expect(await screen.findByLabelText('Next')).toBeEnabled();
  94. await userEvent.click(screen.getByLabelText('Next'));
  95. expect(browserHistory.push).toHaveBeenCalledWith({
  96. pathname: '/mock-pathname/',
  97. query: {
  98. cursor: '0:100:0',
  99. },
  100. });
  101. });
  102. it('should reset pagination cursor on date change', async () => {
  103. createWrapper();
  104. expect(await screen.findByText('Last 7 days')).toBeInTheDocument();
  105. await userEvent.click(screen.getByText('Last 7 days'));
  106. await userEvent.click(screen.getByText('Last 24 hours'));
  107. expect(context.router.push).toHaveBeenCalledWith(
  108. expect.objectContaining({
  109. query: {
  110. pageStatsPeriod: '24h',
  111. cursor: undefined,
  112. pageEnd: undefined,
  113. pageStart: undefined,
  114. pageUtc: undefined,
  115. },
  116. })
  117. );
  118. });
  119. it('should show the time since last triggered in sidebar', async () => {
  120. createWrapper();
  121. expect(await screen.findAllByText('Last Triggered')).toHaveLength(2);
  122. expect(screen.getByText('2 days ago')).toBeInTheDocument();
  123. });
  124. it('renders not found on 404', async () => {
  125. MockApiClient.addMockResponse({
  126. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/`,
  127. statusCode: 404,
  128. body: {},
  129. match: [MockApiClient.matchQuery({expand: 'lastTriggered'})],
  130. });
  131. createWrapper();
  132. expect(
  133. await screen.findByText('The alert rule you were looking for was not found.')
  134. ).toBeInTheDocument();
  135. });
  136. it('renders incompatible rule filter', async () => {
  137. const incompatibleRule = TestStubs.ProjectAlertRule({
  138. conditions: [
  139. {id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition'},
  140. {id: 'sentry.rules.conditions.regression_event.RegressionEventCondition'},
  141. ],
  142. });
  143. MockApiClient.addMockResponse({
  144. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/`,
  145. body: incompatibleRule,
  146. match: [MockApiClient.matchQuery({expand: 'lastTriggered'})],
  147. });
  148. createWrapper();
  149. expect(
  150. await screen.findByText(
  151. 'The conditions in this alert rule conflict and might not be working properly.'
  152. )
  153. ).toBeInTheDocument();
  154. });
  155. it('incompatible rule banner hidden for good rule', async () => {
  156. createWrapper();
  157. expect(await screen.findAllByText('My alert rule')).toHaveLength(2);
  158. expect(
  159. screen.queryByText(
  160. 'The conditions in this alert rule conflict and might not be working properly.'
  161. )
  162. ).not.toBeInTheDocument();
  163. });
  164. it('rule disabled banner because of missing actions and hides some actions', async () => {
  165. MockApiClient.addMockResponse({
  166. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/`,
  167. body: ProjectAlertRule({
  168. actions: [],
  169. status: 'disabled',
  170. }),
  171. match: [MockApiClient.matchQuery({expand: 'lastTriggered'})],
  172. });
  173. createWrapper();
  174. expect(
  175. await screen.findByText(
  176. 'This alert is disabled due to missing actions. Please edit the alert rule to enable this alert.'
  177. )
  178. ).toBeInTheDocument();
  179. expect(screen.getByRole('button', {name: 'Edit to enable'})).toBeInTheDocument();
  180. expect(screen.getByRole('button', {name: 'Duplicate'})).toBeDisabled();
  181. expect(screen.getByRole('button', {name: 'Mute for me'})).toBeDisabled();
  182. });
  183. it('rule disabled banner generic', async () => {
  184. MockApiClient.addMockResponse({
  185. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/`,
  186. body: ProjectAlertRule({
  187. status: 'disabled',
  188. }),
  189. match: [MockApiClient.matchQuery({expand: 'lastTriggered'})],
  190. });
  191. createWrapper();
  192. expect(
  193. await screen.findByText(
  194. 'This alert is disabled due to its configuration and needs to be edited to be enabled.'
  195. )
  196. ).toBeInTheDocument();
  197. });
  198. it('rule to be disabled can opt out', async () => {
  199. const disabledRule = ProjectAlertRule({
  200. disableDate: moment().add(1, 'day').format(),
  201. disableReason: 'noisy',
  202. });
  203. MockApiClient.addMockResponse({
  204. url: `/projects/${organization.slug}/${project.slug}/rules/${disabledRule.id}/`,
  205. body: disabledRule,
  206. match: [MockApiClient.matchQuery({expand: 'lastTriggered'})],
  207. });
  208. const updateMock = MockApiClient.addMockResponse({
  209. url: `/projects/${organization.slug}/${project.slug}/rules/${disabledRule.id}/`,
  210. method: 'PUT',
  211. });
  212. createWrapper();
  213. expect(
  214. await screen.findByText(/This alert is scheduled to be disabled/)
  215. ).toBeInTheDocument();
  216. await userEvent.click(screen.getByRole('button', {name: 'click here'}));
  217. expect(updateMock).toHaveBeenCalledWith(
  218. expect.anything(),
  219. expect.objectContaining({data: {...disabledRule, optOutExplicit: true}})
  220. );
  221. expect(
  222. screen.queryByText(/This alert is scheduled to be disabled/)
  223. ).not.toBeInTheDocument();
  224. });
  225. it('disabled rule can be re-enabled', async () => {
  226. const disabledRule = ProjectAlertRule({
  227. status: 'disabled',
  228. disableDate: moment().subtract(1, 'day').format(),
  229. disableReason: 'noisy',
  230. });
  231. MockApiClient.addMockResponse({
  232. url: `/projects/${organization.slug}/${project.slug}/rules/${disabledRule.id}/`,
  233. body: disabledRule,
  234. match: [MockApiClient.matchQuery({expand: 'lastTriggered'})],
  235. });
  236. const enableMock = MockApiClient.addMockResponse({
  237. url: `/projects/${organization.slug}/${project.slug}/rules/${disabledRule.id}/enable/`,
  238. method: 'PUT',
  239. });
  240. createWrapper();
  241. expect(
  242. await screen.findByText(/This alert was disabled due to lack of activity/)
  243. ).toBeInTheDocument();
  244. await userEvent.click(screen.getByRole('button', {name: 'click here'}));
  245. expect(enableMock).toHaveBeenCalled();
  246. expect(
  247. screen.queryByText(/This alert was disabled due to lack of activity/)
  248. ).not.toBeInTheDocument();
  249. expect(screen.queryByText(/This alert is disabled/)).not.toBeInTheDocument();
  250. });
  251. it('renders the mute button and can mute/unmute alerts', async () => {
  252. const postRequest = MockApiClient.addMockResponse({
  253. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/snooze/`,
  254. method: 'POST',
  255. });
  256. const deleteRequest = MockApiClient.addMockResponse({
  257. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/snooze/`,
  258. method: 'DELETE',
  259. });
  260. createWrapper();
  261. expect(await screen.findByText('Mute for everyone')).toBeInTheDocument();
  262. await userEvent.click(screen.getByRole('button', {name: 'Mute for everyone'}));
  263. expect(postRequest).toHaveBeenCalledWith(
  264. expect.anything(),
  265. expect.objectContaining({data: {target: 'everyone'}})
  266. );
  267. expect(await screen.findByText('Unmute')).toBeInTheDocument();
  268. await userEvent.click(screen.getByRole('button', {name: 'Unmute'}));
  269. expect(deleteRequest).toHaveBeenCalledTimes(1);
  270. });
  271. it('mutes alert if query parameter is set', async () => {
  272. const request = MockApiClient.addMockResponse({
  273. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/snooze/`,
  274. method: 'POST',
  275. });
  276. const contextWithQueryParam = initializeOrg({
  277. router: {
  278. location: {query: {mute: '1'}},
  279. },
  280. });
  281. createWrapper({}, contextWithQueryParam);
  282. expect(await screen.findByText('Unmute')).toBeInTheDocument();
  283. expect(request).toHaveBeenCalledWith(
  284. expect.anything(),
  285. expect.objectContaining({
  286. data: {target: 'everyone'},
  287. })
  288. );
  289. });
  290. it('mute button is disabled if no alerts:write permission', async () => {
  291. const orgWithoutAccess = {
  292. access: [],
  293. };
  294. const contextWithoutAccess = initializeOrg({
  295. organization: orgWithoutAccess,
  296. });
  297. createWrapper({}, contextWithoutAccess, orgWithoutAccess);
  298. expect(await screen.findByRole('button', {name: 'Mute for everyone'})).toBeDisabled();
  299. });
  300. it('inserts user email into rule notify action', async () => {
  301. // Alert rule with "send a notification to member" action
  302. const sendNotificationRule = TestStubs.ProjectAlertRule({
  303. actions: [
  304. {
  305. id: 'sentry.mail.actions.NotifyEmailAction',
  306. name: 'Send a notification to Member and if none can be found then send a notification to ActiveMembers',
  307. targetIdentifier: member.id,
  308. targetType: 'Member',
  309. },
  310. ],
  311. });
  312. MockApiClient.addMockResponse({
  313. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/`,
  314. body: sendNotificationRule,
  315. });
  316. createWrapper();
  317. expect(
  318. await screen.findByText(`Send a notification to ${member.email}`)
  319. ).toBeInTheDocument();
  320. });
  321. });