issueRuleEditor.spec.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import {browserHistory} from 'react-router';
  2. import selectEvent from 'react-select-event';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {
  5. render,
  6. renderGlobalModal,
  7. screen,
  8. userEvent,
  9. waitFor,
  10. } from 'sentry-test/reactTestingLibrary';
  11. import {
  12. addErrorMessage,
  13. addLoadingMessage,
  14. addSuccessMessage,
  15. } from 'sentry/actionCreators/indicator';
  16. import {updateOnboardingTask} from 'sentry/actionCreators/onboardingTasks';
  17. import {metric} from 'sentry/utils/analytics';
  18. import IssueRuleEditor from 'sentry/views/alerts/rules/issue';
  19. import ProjectAlerts from 'sentry/views/settings/projectAlerts';
  20. jest.unmock('sentry/utils/recreateRoute');
  21. jest.mock('sentry/actionCreators/onboardingTasks');
  22. jest.mock('sentry/actionCreators/indicator', () => ({
  23. addSuccessMessage: jest.fn(),
  24. addErrorMessage: jest.fn(),
  25. addLoadingMessage: jest.fn(),
  26. }));
  27. jest.mock('sentry/utils/analytics', () => ({
  28. metric: {
  29. startTransaction: jest.fn(() => ({
  30. setTag: jest.fn(),
  31. setData: jest.fn(),
  32. })),
  33. endTransaction: jest.fn(),
  34. mark: jest.fn(),
  35. measure: jest.fn(),
  36. },
  37. }));
  38. const projectAlertRuleDetailsRoutes = [
  39. {
  40. path: '/',
  41. },
  42. {
  43. path: '/settings/',
  44. name: 'Settings',
  45. indexRoute: {},
  46. },
  47. {
  48. name: 'Organization',
  49. path: ':orgId/',
  50. },
  51. {
  52. name: 'Project',
  53. path: 'projects/:projectId/',
  54. },
  55. {},
  56. {
  57. indexRoute: {name: 'General'},
  58. },
  59. {
  60. name: 'Alert Rules',
  61. path: 'alerts/',
  62. indexRoute: {},
  63. },
  64. {
  65. path: 'rules/',
  66. name: 'Rules',
  67. component: null,
  68. indexRoute: {},
  69. childRoutes: [
  70. {path: 'new/', name: 'New'},
  71. {path: ':ruleId/', name: 'Edit'},
  72. ],
  73. },
  74. {path: ':ruleId/', name: 'Edit Alert Rule'},
  75. ];
  76. const createWrapper = (props = {}) => {
  77. const {organization, project, routerContext, router} = initializeOrg(props);
  78. const params = {
  79. orgId: organization.slug,
  80. projectId: project.slug,
  81. ruleId: router.location.query.createFromDuplicate ? undefined : '1',
  82. };
  83. const onChangeTitleMock = jest.fn();
  84. const wrapper = render(
  85. <ProjectAlerts organization={organization} params={params}>
  86. <IssueRuleEditor
  87. params={params}
  88. location={router.location}
  89. routes={projectAlertRuleDetailsRoutes}
  90. router={router}
  91. onChangeTitle={onChangeTitleMock}
  92. project={project}
  93. userTeamIds={[]}
  94. />
  95. </ProjectAlerts>,
  96. {context: routerContext}
  97. );
  98. return {
  99. wrapper,
  100. organization,
  101. project,
  102. onChangeTitleMock,
  103. };
  104. };
  105. describe('ProjectAlerts -> IssueRuleEditor', function () {
  106. beforeEach(function () {
  107. browserHistory.replace = jest.fn();
  108. MockApiClient.addMockResponse({
  109. url: '/projects/org-slug/project-slug/rules/configuration/',
  110. body: TestStubs.ProjectAlertRuleConfiguration(),
  111. });
  112. MockApiClient.addMockResponse({
  113. url: '/projects/org-slug/project-slug/rules/1/',
  114. body: TestStubs.ProjectAlertRule(),
  115. });
  116. MockApiClient.addMockResponse({
  117. url: '/projects/org-slug/project-slug/environments/',
  118. body: TestStubs.Environments(),
  119. });
  120. MockApiClient.addMockResponse({
  121. url: `/projects/org-slug/project-slug/?expand=hasAlertIntegration`,
  122. body: {},
  123. });
  124. MockApiClient.addMockResponse({
  125. url: `/projects/org-slug/project-slug/ownership/`,
  126. method: 'GET',
  127. body: {
  128. fallthrough: false,
  129. autoAssignment: false,
  130. },
  131. });
  132. });
  133. afterEach(function () {
  134. MockApiClient.clearMockResponses();
  135. jest.clearAllMocks();
  136. });
  137. describe('Edit Rule', function () {
  138. let mock;
  139. const endpoint = '/projects/org-slug/project-slug/rules/1/';
  140. beforeEach(function () {
  141. mock = MockApiClient.addMockResponse({
  142. url: endpoint,
  143. method: 'PUT',
  144. body: TestStubs.ProjectAlertRule(),
  145. });
  146. });
  147. it('gets correct rule name', function () {
  148. const rule = TestStubs.ProjectAlertRule();
  149. mock = MockApiClient.addMockResponse({
  150. url: endpoint,
  151. method: 'GET',
  152. body: rule,
  153. });
  154. const {onChangeTitleMock} = createWrapper();
  155. expect(mock).toHaveBeenCalled();
  156. expect(onChangeTitleMock).toHaveBeenCalledWith(rule.name);
  157. });
  158. it('deletes rule', async function () {
  159. const deleteMock = MockApiClient.addMockResponse({
  160. url: endpoint,
  161. method: 'DELETE',
  162. body: {},
  163. });
  164. createWrapper();
  165. renderGlobalModal();
  166. userEvent.click(screen.getByLabelText('Delete Rule'));
  167. expect(
  168. await screen.findByText('Are you sure you want to delete this rule?')
  169. ).toBeInTheDocument();
  170. userEvent.click(screen.getByTestId('confirm-button'));
  171. await waitFor(() => expect(deleteMock).toHaveBeenCalled());
  172. expect(browserHistory.replace).toHaveBeenCalledWith(
  173. '/settings/org-slug/projects/project-slug/alerts/'
  174. );
  175. });
  176. it('sends correct environment value', async function () {
  177. createWrapper();
  178. await selectEvent.select(screen.getByText('staging'), 'production');
  179. userEvent.click(screen.getByText('Save Rule'));
  180. await waitFor(() =>
  181. expect(mock).toHaveBeenCalledWith(
  182. endpoint,
  183. expect.objectContaining({
  184. data: expect.objectContaining({environment: 'production'}),
  185. })
  186. )
  187. );
  188. expect(metric.startTransaction).toHaveBeenCalledTimes(1);
  189. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  190. });
  191. it('strips environment value if "All environments" is selected', async function () {
  192. createWrapper();
  193. await selectEvent.select(screen.getByText('staging'), 'All Environments');
  194. userEvent.click(screen.getByText('Save Rule'));
  195. await waitFor(() => expect(mock).toHaveBeenCalledTimes(1));
  196. expect(mock).not.toHaveBeenCalledWith(
  197. endpoint,
  198. expect.objectContaining({
  199. data: expect.objectContaining({environment: '__all_environments__'}),
  200. })
  201. );
  202. expect(metric.startTransaction).toHaveBeenCalledTimes(1);
  203. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  204. });
  205. it('updates the alert onboarding task', async function () {
  206. createWrapper();
  207. userEvent.click(screen.getByText('Save Rule'));
  208. await waitFor(() => expect(updateOnboardingTask).toHaveBeenCalledTimes(1));
  209. expect(metric.startTransaction).toHaveBeenCalledTimes(1);
  210. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  211. });
  212. });
  213. describe('Edit Rule: Slack Channel Look Up', function () {
  214. const uuid = 'xxxx-xxxx-xxxx';
  215. beforeEach(function () {
  216. jest.useFakeTimers();
  217. });
  218. afterEach(function () {
  219. jest.clearAllTimers();
  220. MockApiClient.clearMockResponses();
  221. });
  222. it('success status updates the rule', async function () {
  223. const mockSuccess = MockApiClient.addMockResponse({
  224. url: `/projects/org-slug/project-slug/rule-task/${uuid}/`,
  225. body: {status: 'success', rule: TestStubs.ProjectAlertRule({name: 'Slack Rule'})},
  226. });
  227. MockApiClient.addMockResponse({
  228. url: '/projects/org-slug/project-slug/rules/1/',
  229. method: 'PUT',
  230. statusCode: 202,
  231. body: {uuid},
  232. });
  233. createWrapper();
  234. userEvent.click(screen.getByText('Save Rule'));
  235. await waitFor(() => expect(addLoadingMessage).toHaveBeenCalledTimes(2));
  236. jest.advanceTimersByTime(1000);
  237. await waitFor(() => expect(mockSuccess).toHaveBeenCalledTimes(1));
  238. jest.advanceTimersByTime(1000);
  239. await waitFor(() => expect(addSuccessMessage).toHaveBeenCalledTimes(1));
  240. expect(screen.getByDisplayValue('Slack Rule')).toBeInTheDocument();
  241. });
  242. it('pending status keeps loading true', async function () {
  243. const pollingMock = MockApiClient.addMockResponse({
  244. url: `/projects/org-slug/project-slug/rule-task/${uuid}/`,
  245. body: {status: 'pending'},
  246. });
  247. MockApiClient.addMockResponse({
  248. url: '/projects/org-slug/project-slug/rules/1/',
  249. method: 'PUT',
  250. statusCode: 202,
  251. body: {uuid},
  252. });
  253. createWrapper();
  254. userEvent.click(screen.getByText('Save Rule'));
  255. await waitFor(() => expect(addLoadingMessage).toHaveBeenCalledTimes(2));
  256. jest.advanceTimersByTime(1000);
  257. await waitFor(() => expect(pollingMock).toHaveBeenCalledTimes(1));
  258. expect(screen.getByTestId('loading-mask')).toBeInTheDocument();
  259. });
  260. it('failed status renders error message', async function () {
  261. const mockFailed = MockApiClient.addMockResponse({
  262. url: `/projects/org-slug/project-slug/rule-task/${uuid}/`,
  263. body: {status: 'failed'},
  264. });
  265. MockApiClient.addMockResponse({
  266. url: '/projects/org-slug/project-slug/rules/1/',
  267. method: 'PUT',
  268. statusCode: 202,
  269. body: {uuid},
  270. });
  271. createWrapper();
  272. userEvent.click(screen.getByText('Save Rule'));
  273. await waitFor(() => expect(addLoadingMessage).toHaveBeenCalledTimes(2));
  274. jest.advanceTimersByTime(1000);
  275. await waitFor(() => expect(mockFailed).toHaveBeenCalledTimes(1));
  276. expect(screen.getByText('An error occurred')).toBeInTheDocument();
  277. expect(addErrorMessage).toHaveBeenCalledTimes(1);
  278. });
  279. });
  280. describe('Duplicate Rule', function () {
  281. let mock;
  282. const rule = TestStubs.ProjectAlertRule();
  283. const endpoint = `/projects/org-slug/project-slug/rules/${rule.id}/`;
  284. beforeEach(function () {
  285. mock = MockApiClient.addMockResponse({
  286. url: endpoint,
  287. method: 'GET',
  288. body: rule,
  289. });
  290. });
  291. it('gets correct rule to duplicate and renders fields correctly', function () {
  292. createWrapper({
  293. organization: {
  294. access: ['alerts:write'],
  295. features: ['duplicate-alert-rule'],
  296. },
  297. router: {
  298. location: {
  299. query: {
  300. createFromDuplicate: true,
  301. duplicateRuleId: `${rule.id}`,
  302. },
  303. },
  304. },
  305. });
  306. expect(mock).toHaveBeenCalled();
  307. expect(screen.getByTestId('alert-name')).toHaveValue(`${rule.name} copy`);
  308. });
  309. });
  310. });