ruleDetails.spec.tsx 12 KB

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