issueRuleEditor.spec.jsx 10 KB

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