ruleDetails.spec.tsx 12 KB

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