index.spec.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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. trackAnalytics: jest.fn(),
  39. }));
  40. const projectAlertRuleDetailsRoutes = [
  41. {
  42. path: '/',
  43. },
  44. {
  45. path: '/settings/',
  46. name: 'Settings',
  47. indexRoute: {},
  48. },
  49. {
  50. name: 'Organization',
  51. path: ':orgId/',
  52. },
  53. {
  54. name: 'Project',
  55. path: 'projects/:projectId/',
  56. },
  57. {},
  58. {
  59. indexRoute: {name: 'General'},
  60. },
  61. {
  62. name: 'Alert Rules',
  63. path: 'alerts/',
  64. indexRoute: {},
  65. },
  66. {
  67. path: 'rules/',
  68. name: 'Rules',
  69. component: null,
  70. indexRoute: {},
  71. childRoutes: [
  72. {path: 'new/', name: 'New'},
  73. {path: ':ruleId/', name: 'Edit'},
  74. ],
  75. },
  76. {path: ':ruleId/', name: 'Edit Alert Rule'},
  77. ];
  78. const createWrapper = (props = {}) => {
  79. const {organization, project, routerContext, router} = initializeOrg(props);
  80. const params = {
  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} project={project} 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. MockApiClient.addMockResponse({
  134. url: '/projects/org-slug/project-slug/rules/preview/',
  135. method: 'POST',
  136. body: [],
  137. });
  138. ProjectsStore.loadInitialData([TestStubs.Project()]);
  139. });
  140. afterEach(function () {
  141. MockApiClient.clearMockResponses();
  142. jest.clearAllMocks();
  143. ProjectsStore.reset();
  144. });
  145. describe('Edit Rule', function () {
  146. let mock;
  147. const endpoint = '/projects/org-slug/project-slug/rules/1/';
  148. beforeEach(function () {
  149. mock = MockApiClient.addMockResponse({
  150. url: endpoint,
  151. method: 'PUT',
  152. body: TestStubs.ProjectAlertRule(),
  153. });
  154. });
  155. it('gets correct rule name', function () {
  156. const rule = TestStubs.ProjectAlertRule();
  157. mock = MockApiClient.addMockResponse({
  158. url: endpoint,
  159. method: 'GET',
  160. body: rule,
  161. });
  162. const {onChangeTitleMock} = createWrapper();
  163. expect(mock).toHaveBeenCalled();
  164. expect(onChangeTitleMock).toHaveBeenCalledWith(rule.name);
  165. });
  166. it('deletes rule', async function () {
  167. const deleteMock = MockApiClient.addMockResponse({
  168. url: endpoint,
  169. method: 'DELETE',
  170. body: {},
  171. });
  172. createWrapper();
  173. renderGlobalModal();
  174. await userEvent.click(screen.getByLabelText('Delete Rule'));
  175. expect(
  176. await screen.findByText('Are you sure you want to delete this rule?')
  177. ).toBeInTheDocument();
  178. await userEvent.click(screen.getByTestId('confirm-button'));
  179. await waitFor(() => expect(deleteMock).toHaveBeenCalled());
  180. expect(browserHistory.replace).toHaveBeenCalledWith(
  181. '/settings/org-slug/projects/project-slug/alerts/'
  182. );
  183. });
  184. it('sends correct environment value', async function () {
  185. createWrapper();
  186. await selectEvent.select(screen.getByText('staging'), 'production');
  187. await userEvent.click(screen.getByText('Save Rule'));
  188. await waitFor(() =>
  189. expect(mock).toHaveBeenCalledWith(
  190. endpoint,
  191. expect.objectContaining({
  192. data: expect.objectContaining({environment: 'production'}),
  193. })
  194. )
  195. );
  196. expect(metric.startTransaction).toHaveBeenCalledTimes(1);
  197. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  198. });
  199. it('strips environment value if "All environments" is selected', async function () {
  200. createWrapper();
  201. await selectEvent.select(screen.getByText('staging'), 'All Environments');
  202. await userEvent.click(screen.getByText('Save Rule'));
  203. await waitFor(() => expect(mock).toHaveBeenCalledTimes(1));
  204. expect(mock).not.toHaveBeenCalledWith(
  205. endpoint,
  206. expect.objectContaining({
  207. data: expect.objectContaining({environment: '__all_environments__'}),
  208. })
  209. );
  210. expect(metric.startTransaction).toHaveBeenCalledTimes(1);
  211. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  212. });
  213. it('updates the alert onboarding task', async function () {
  214. createWrapper();
  215. await userEvent.click(screen.getByText('Save Rule'));
  216. await waitFor(() => expect(updateOnboardingTask).toHaveBeenCalledTimes(1));
  217. expect(metric.startTransaction).toHaveBeenCalledTimes(1);
  218. expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
  219. });
  220. it('renders multiple sentry apps at the same time', async () => {
  221. const linearApp = {
  222. id: 'sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction',
  223. enabled: true,
  224. actionType: 'sentryapp',
  225. service: 'linear',
  226. sentryAppInstallationUuid: 'linear-d864bc2a8755',
  227. prompt: 'Linear',
  228. label: 'Create a Linear issue with these ',
  229. formFields: {
  230. type: 'alert-rule-settings',
  231. uri: '/hooks/sentry/alert-rule-action',
  232. description:
  233. 'When the alert fires automatically create a Linear issue with the following properties.',
  234. required_fields: [
  235. {
  236. name: 'teamId',
  237. label: 'Team',
  238. type: 'select',
  239. uri: '/hooks/sentry/issues/teams',
  240. choices: [['test-6f0b2b4d402b', 'Sentry']],
  241. },
  242. ],
  243. optional_fields: [
  244. // Optional fields removed
  245. ],
  246. },
  247. };
  248. const threadsApp = {
  249. id: 'sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction',
  250. enabled: true,
  251. actionType: 'sentryapp',
  252. service: 'threads',
  253. sentryAppInstallationUuid: 'threads-987c470e50cc',
  254. prompt: 'Threads',
  255. label: 'Post to a Threads channel with these ',
  256. formFields: {
  257. type: 'alert-rule-settings',
  258. uri: '/sentry/saveAlert',
  259. required_fields: [
  260. {
  261. type: 'select',
  262. label: 'Channel',
  263. name: 'channel',
  264. async: true,
  265. uri: '/sentry/channels',
  266. choices: [],
  267. },
  268. ],
  269. },
  270. };
  271. MockApiClient.addMockResponse({
  272. url: '/projects/org-slug/project-slug/rules/configuration/',
  273. body: {actions: [linearApp, threadsApp], conditions: [], filters: []},
  274. });
  275. createWrapper();
  276. await selectEvent.select(screen.getByText('Add action...'), 'Threads');
  277. await selectEvent.select(screen.getByText('Add action...'), 'Linear');
  278. expect(screen.getByText('Create a Linear issue with these')).toBeInTheDocument();
  279. expect(
  280. screen.getByText('Post to a Threads channel with these')
  281. ).toBeInTheDocument();
  282. });
  283. });
  284. describe('Edit Rule: Slack Channel Look Up', function () {
  285. const uuid = 'xxxx-xxxx-xxxx';
  286. beforeEach(function () {
  287. jest.useFakeTimers();
  288. });
  289. afterEach(function () {
  290. jest.clearAllTimers();
  291. MockApiClient.clearMockResponses();
  292. });
  293. it('success status updates the rule', async function () {
  294. const mockSuccess = MockApiClient.addMockResponse({
  295. url: `/projects/org-slug/project-slug/rule-task/${uuid}/`,
  296. body: {status: 'success', rule: TestStubs.ProjectAlertRule({name: 'Slack Rule'})},
  297. });
  298. MockApiClient.addMockResponse({
  299. url: '/projects/org-slug/project-slug/rules/1/',
  300. method: 'PUT',
  301. statusCode: 202,
  302. body: {uuid},
  303. });
  304. createWrapper();
  305. await userEvent.click(screen.getByText('Save Rule'), {delay: null});
  306. await waitFor(() => expect(addLoadingMessage).toHaveBeenCalledTimes(2));
  307. jest.advanceTimersByTime(1000);
  308. await waitFor(() => expect(mockSuccess).toHaveBeenCalledTimes(1));
  309. jest.advanceTimersByTime(1000);
  310. await waitFor(() => expect(addSuccessMessage).toHaveBeenCalledTimes(1));
  311. expect(screen.getByDisplayValue('Slack Rule')).toBeInTheDocument();
  312. });
  313. it('pending status keeps loading true', async function () {
  314. const pollingMock = MockApiClient.addMockResponse({
  315. url: `/projects/org-slug/project-slug/rule-task/${uuid}/`,
  316. body: {status: 'pending'},
  317. });
  318. MockApiClient.addMockResponse({
  319. url: '/projects/org-slug/project-slug/rules/1/',
  320. method: 'PUT',
  321. statusCode: 202,
  322. body: {uuid},
  323. });
  324. createWrapper();
  325. await userEvent.click(screen.getByText('Save Rule'), {delay: null});
  326. await waitFor(() => expect(addLoadingMessage).toHaveBeenCalledTimes(2));
  327. jest.advanceTimersByTime(1000);
  328. await waitFor(() => expect(pollingMock).toHaveBeenCalledTimes(1));
  329. expect(screen.getByTestId('loading-mask')).toBeInTheDocument();
  330. });
  331. it('failed status renders error message', async function () {
  332. const mockFailed = MockApiClient.addMockResponse({
  333. url: `/projects/org-slug/project-slug/rule-task/${uuid}/`,
  334. body: {status: 'failed'},
  335. });
  336. MockApiClient.addMockResponse({
  337. url: '/projects/org-slug/project-slug/rules/1/',
  338. method: 'PUT',
  339. statusCode: 202,
  340. body: {uuid},
  341. });
  342. createWrapper();
  343. await userEvent.click(screen.getByText('Save Rule'), {delay: null});
  344. await waitFor(() => expect(addLoadingMessage).toHaveBeenCalledTimes(2));
  345. jest.advanceTimersByTime(1000);
  346. await waitFor(() => expect(mockFailed).toHaveBeenCalledTimes(1));
  347. expect(screen.getByText('An error occurred')).toBeInTheDocument();
  348. expect(addErrorMessage).toHaveBeenCalledTimes(1);
  349. });
  350. });
  351. describe('Duplicate Rule', function () {
  352. let mock;
  353. const rule = TestStubs.ProjectAlertRule();
  354. const endpoint = `/projects/org-slug/project-slug/rules/${rule.id}/`;
  355. beforeEach(function () {
  356. mock = MockApiClient.addMockResponse({
  357. url: endpoint,
  358. method: 'GET',
  359. body: rule,
  360. });
  361. });
  362. it('gets correct rule to duplicate and renders fields correctly', function () {
  363. createWrapper({
  364. organization: {
  365. access: ['alerts:write'],
  366. },
  367. router: {
  368. location: {
  369. query: {
  370. createFromDuplicate: true,
  371. duplicateRuleId: `${rule.id}`,
  372. },
  373. },
  374. },
  375. });
  376. expect(mock).toHaveBeenCalled();
  377. expect(screen.getByTestId('alert-name')).toHaveValue(`${rule.name} copy`);
  378. });
  379. });
  380. });