ruleDetails.spec.tsx 12 KB

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