ruleDetails.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  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. const dateSelector = await screen.findByText('7D');
  106. expect(dateSelector).toBeInTheDocument();
  107. await userEvent.click(dateSelector);
  108. await userEvent.click(screen.getByRole('option', {name: 'Last 24 hours'}));
  109. expect(context.router.push).toHaveBeenCalledWith(
  110. expect.objectContaining({
  111. query: {
  112. pageStatsPeriod: '24h',
  113. cursor: undefined,
  114. pageEnd: undefined,
  115. pageStart: undefined,
  116. pageUtc: undefined,
  117. },
  118. })
  119. );
  120. });
  121. it('should show the time since last triggered in sidebar', async () => {
  122. createWrapper();
  123. expect(await screen.findAllByText('Last Triggered')).toHaveLength(2);
  124. expect(screen.getByText('2 days ago')).toBeInTheDocument();
  125. });
  126. it('renders not found on 404', async () => {
  127. MockApiClient.addMockResponse({
  128. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/`,
  129. statusCode: 404,
  130. body: {},
  131. match: [MockApiClient.matchQuery({expand: 'lastTriggered'})],
  132. });
  133. createWrapper();
  134. expect(
  135. await screen.findByText('The alert rule you were looking for was not found.')
  136. ).toBeInTheDocument();
  137. });
  138. it('renders incompatible rule filter', async () => {
  139. const incompatibleRule = TestStubs.ProjectAlertRule({
  140. conditions: [
  141. {id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition'},
  142. {id: 'sentry.rules.conditions.regression_event.RegressionEventCondition'},
  143. ],
  144. });
  145. MockApiClient.addMockResponse({
  146. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/`,
  147. body: incompatibleRule,
  148. match: [MockApiClient.matchQuery({expand: 'lastTriggered'})],
  149. });
  150. createWrapper();
  151. expect(
  152. await screen.findByText(
  153. 'The conditions in this alert rule conflict and might not be working properly.'
  154. )
  155. ).toBeInTheDocument();
  156. });
  157. it('incompatible rule banner hidden for good rule', async () => {
  158. createWrapper();
  159. expect(await screen.findAllByText('My alert rule')).toHaveLength(2);
  160. expect(
  161. screen.queryByText(
  162. 'The conditions in this alert rule conflict and might not be working properly.'
  163. )
  164. ).not.toBeInTheDocument();
  165. });
  166. it('rule disabled banner because of missing actions and hides some actions', async () => {
  167. MockApiClient.addMockResponse({
  168. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/`,
  169. body: ProjectAlertRule({
  170. actions: [],
  171. status: 'disabled',
  172. }),
  173. match: [MockApiClient.matchQuery({expand: 'lastTriggered'})],
  174. });
  175. createWrapper();
  176. expect(
  177. await screen.findByText(
  178. 'This alert is disabled due to missing actions. Please edit the alert rule to enable this alert.'
  179. )
  180. ).toBeInTheDocument();
  181. expect(screen.getByRole('button', {name: 'Edit to enable'})).toBeInTheDocument();
  182. expect(screen.getByRole('button', {name: 'Duplicate'})).toBeDisabled();
  183. expect(screen.getByRole('button', {name: 'Mute for me'})).toBeDisabled();
  184. });
  185. it('rule disabled banner generic', async () => {
  186. MockApiClient.addMockResponse({
  187. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/`,
  188. body: ProjectAlertRule({
  189. status: 'disabled',
  190. }),
  191. match: [MockApiClient.matchQuery({expand: 'lastTriggered'})],
  192. });
  193. createWrapper();
  194. expect(
  195. await screen.findByText(
  196. 'This alert is disabled due to its configuration and needs to be edited to be enabled.'
  197. )
  198. ).toBeInTheDocument();
  199. });
  200. it('rule to be disabled can opt out', async () => {
  201. const disabledRule = ProjectAlertRule({
  202. disableDate: moment().add(1, 'day').format(),
  203. disableReason: 'noisy',
  204. });
  205. MockApiClient.addMockResponse({
  206. url: `/projects/${organization.slug}/${project.slug}/rules/${disabledRule.id}/`,
  207. body: disabledRule,
  208. match: [MockApiClient.matchQuery({expand: 'lastTriggered'})],
  209. });
  210. const updateMock = MockApiClient.addMockResponse({
  211. url: `/projects/${organization.slug}/${project.slug}/rules/${disabledRule.id}/`,
  212. method: 'PUT',
  213. });
  214. createWrapper();
  215. expect(
  216. await screen.findByText(/This alert is scheduled to be disabled/)
  217. ).toBeInTheDocument();
  218. await userEvent.click(screen.getByRole('button', {name: 'click here'}));
  219. expect(updateMock).toHaveBeenCalledWith(
  220. expect.anything(),
  221. expect.objectContaining({data: {...disabledRule, optOutExplicit: true}})
  222. );
  223. expect(
  224. screen.queryByText(/This alert is scheduled to be disabled/)
  225. ).not.toBeInTheDocument();
  226. });
  227. it('disabled rule can be re-enabled', async () => {
  228. const disabledRule = ProjectAlertRule({
  229. status: 'disabled',
  230. disableDate: moment().subtract(1, 'day').format(),
  231. disableReason: 'noisy',
  232. });
  233. MockApiClient.addMockResponse({
  234. url: `/projects/${organization.slug}/${project.slug}/rules/${disabledRule.id}/`,
  235. body: disabledRule,
  236. match: [MockApiClient.matchQuery({expand: 'lastTriggered'})],
  237. });
  238. const enableMock = MockApiClient.addMockResponse({
  239. url: `/projects/${organization.slug}/${project.slug}/rules/${disabledRule.id}/enable/`,
  240. method: 'PUT',
  241. });
  242. createWrapper();
  243. expect(
  244. await screen.findByText(/This alert was disabled due to lack of activity/)
  245. ).toBeInTheDocument();
  246. await userEvent.click(screen.getByRole('button', {name: 'click here'}));
  247. expect(enableMock).toHaveBeenCalled();
  248. expect(
  249. screen.queryByText(/This alert was disabled due to lack of activity/)
  250. ).not.toBeInTheDocument();
  251. expect(screen.queryByText(/This alert is disabled/)).not.toBeInTheDocument();
  252. });
  253. it('renders the mute button and can mute/unmute alerts', async () => {
  254. const postRequest = MockApiClient.addMockResponse({
  255. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/snooze/`,
  256. method: 'POST',
  257. });
  258. const deleteRequest = MockApiClient.addMockResponse({
  259. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/snooze/`,
  260. method: 'DELETE',
  261. });
  262. createWrapper();
  263. expect(await screen.findByText('Mute for everyone')).toBeInTheDocument();
  264. await userEvent.click(screen.getByRole('button', {name: 'Mute for everyone'}));
  265. expect(postRequest).toHaveBeenCalledWith(
  266. expect.anything(),
  267. expect.objectContaining({data: {target: 'everyone'}})
  268. );
  269. expect(await screen.findByText('Unmute')).toBeInTheDocument();
  270. await userEvent.click(screen.getByRole('button', {name: 'Unmute'}));
  271. expect(deleteRequest).toHaveBeenCalledTimes(1);
  272. });
  273. it('mutes alert if query parameter is set', async () => {
  274. const request = MockApiClient.addMockResponse({
  275. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/snooze/`,
  276. method: 'POST',
  277. });
  278. const contextWithQueryParam = initializeOrg({
  279. router: {
  280. location: {query: {mute: '1'}},
  281. },
  282. });
  283. createWrapper({}, contextWithQueryParam);
  284. expect(await screen.findByText('Unmute')).toBeInTheDocument();
  285. expect(request).toHaveBeenCalledWith(
  286. expect.anything(),
  287. expect.objectContaining({
  288. data: {target: 'everyone'},
  289. })
  290. );
  291. });
  292. it('mute button is disabled if no alerts:write permission', async () => {
  293. const orgWithoutAccess = Organization({
  294. access: [],
  295. });
  296. const contextWithoutAccess = initializeOrg({
  297. organization: orgWithoutAccess,
  298. });
  299. createWrapper({}, contextWithoutAccess, orgWithoutAccess);
  300. expect(await screen.findByRole('button', {name: 'Mute for everyone'})).toBeDisabled();
  301. });
  302. it('inserts user email into rule notify action', async () => {
  303. // Alert rule with "send a notification to member" action
  304. const sendNotificationRule = TestStubs.ProjectAlertRule({
  305. actions: [
  306. {
  307. id: 'sentry.mail.actions.NotifyEmailAction',
  308. name: 'Send a notification to Member and if none can be found then send a notification to ActiveMembers',
  309. targetIdentifier: member.id,
  310. targetType: 'Member',
  311. },
  312. ],
  313. });
  314. MockApiClient.addMockResponse({
  315. url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/`,
  316. body: sendNotificationRule,
  317. });
  318. createWrapper();
  319. expect(
  320. await screen.findByText(`Send a notification to ${member.email}`)
  321. ).toBeInTheDocument();
  322. });
  323. });