spikeProtectionProjects.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import {AvailableNotificationActionsFixture} from 'sentry-fixture/availableNotificationActions';
  2. import {ProjectFixture} from 'getsentry-test/fixtures/project';
  3. import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
  4. import {initializeOrg} from 'sentry-test/initializeOrg';
  5. import {
  6. cleanup,
  7. render,
  8. renderGlobalModal,
  9. screen,
  10. userEvent,
  11. } from 'sentry-test/reactTestingLibrary';
  12. import type {Project} from 'sentry/types/project';
  13. import SubscriptionStore from 'getsentry/stores/subscriptionStore';
  14. import {SPIKE_PROTECTION_OPTION_DISABLED} from 'getsentry/views/spikeProtection/constants';
  15. import SpikeProtectionProjects from 'getsentry/views/spikeProtection/spikeProtectionProjects';
  16. describe('project renders and toggles', () => {
  17. const projects = [
  18. ProjectFixture({
  19. id: '1',
  20. slug: 'project1',
  21. // If the project option is True, the feature is disabled
  22. options: {[SPIKE_PROTECTION_OPTION_DISABLED]: true},
  23. access: ['project:read'],
  24. }),
  25. ProjectFixture({
  26. id: '2',
  27. slug: 'project2',
  28. // If the project option is False, the feature is Enabled
  29. options: {[SPIKE_PROTECTION_OPTION_DISABLED]: false},
  30. access: ['project:read', 'project:write', 'project:admin'],
  31. }),
  32. ];
  33. let organization: any,
  34. router: any,
  35. mockGet: any,
  36. mockPost: any,
  37. mockDelete: any,
  38. mockDeleteAll: any,
  39. mockPostAll: any,
  40. mockGetProjectNotificationActions: any;
  41. beforeEach(() => {
  42. MockApiClient.clearMockResponses();
  43. const newData = initializeOrg({
  44. projects,
  45. organization: {
  46. features: ['global-views'],
  47. openMembership: true,
  48. access: ['org:write'],
  49. },
  50. });
  51. organization = newData.organization;
  52. router = newData.router;
  53. SubscriptionStore.set(organization.slug, SubscriptionFixture({organization}));
  54. mockGet = MockApiClient.addMockResponse({
  55. url: `/organizations/${organization.slug}/projects/`,
  56. method: 'GET',
  57. body: projects,
  58. statusCode: 200,
  59. });
  60. mockPost = MockApiClient.addMockResponse({
  61. url: `/organizations/${organization.slug}/spike-protections/`,
  62. method: 'POST',
  63. body: [],
  64. statusCode: 200,
  65. });
  66. mockPostAll = MockApiClient.addMockResponse({
  67. url: `/organizations/${organization.slug}/spike-protections/?projectSlug=$all`,
  68. method: 'POST',
  69. body: [],
  70. statusCode: 200,
  71. });
  72. mockDelete = MockApiClient.addMockResponse({
  73. url: `/organizations/${organization.slug}/spike-protections/`,
  74. method: 'DELETE',
  75. body: [],
  76. statusCode: 200,
  77. });
  78. mockDeleteAll = MockApiClient.addMockResponse({
  79. url: `/organizations/${organization.slug}/spike-protections/?projectSlug=$all`,
  80. method: 'DELETE',
  81. body: [],
  82. statusCode: 200,
  83. });
  84. MockApiClient.addMockResponse({
  85. url: `/organizations/${organization.slug}/notifications/available-actions/`,
  86. method: 'GET',
  87. body: AvailableNotificationActionsFixture(),
  88. statusCode: 200,
  89. });
  90. mockGetProjectNotificationActions = MockApiClient.addMockResponse({
  91. url: `/organizations/${organization.slug}/notifications/actions/`,
  92. match: [
  93. MockApiClient.matchQuery({
  94. triggerType: 'spike-protection',
  95. project: projects[0]!.id,
  96. }),
  97. ],
  98. method: 'GET',
  99. body: [
  100. {
  101. id: 2,
  102. organizationId: parseInt(organization.id, 10),
  103. integrationId: null,
  104. sentryAppId: null,
  105. projects: [parseInt(projects[0]!.id, 10)],
  106. serviceType: 'sentry_notification',
  107. triggerType: 'spike-protection',
  108. targetType: 'specific',
  109. targetIdentifier: 'default',
  110. targetDisplay: 'default',
  111. },
  112. ],
  113. statusCode: 200,
  114. });
  115. });
  116. afterEach(() => {
  117. cleanup();
  118. });
  119. async function validateComponents(project: Project, isEnabled: boolean) {
  120. const toggle = await screen.findByTestId(`${project.slug}-spike-protection-toggle`);
  121. if (isEnabled) {
  122. expect(toggle).toBeChecked();
  123. } else {
  124. expect(toggle).not.toBeChecked();
  125. }
  126. return {toggle};
  127. }
  128. it('renders projects table even with no projects', async () => {
  129. const mockGetNoProjects = MockApiClient.addMockResponse({
  130. url: `/organizations/${organization.slug}/projects/`,
  131. method: 'GET',
  132. body: [],
  133. statusCode: 200,
  134. });
  135. render(<SpikeProtectionProjects />, {router});
  136. expect(mockGetNoProjects).toHaveBeenCalled();
  137. expect(await screen.findByText('There are no items to display')).toBeInTheDocument();
  138. });
  139. it('renders projects accessible to user under closed membership', async () => {
  140. organization.openMembership = false;
  141. render(<SpikeProtectionProjects />, {organization});
  142. expect(await screen.findByText('project1')).toBeInTheDocument();
  143. const requestOptions = mockGet.mock.calls[0][1];
  144. expect(requestOptions.query).toEqual(
  145. expect.objectContaining({query: ' is_member:1'})
  146. );
  147. });
  148. it('renders all projects for superuser', async () => {
  149. // even under closed membership
  150. organization.access = ['org:superuser'];
  151. render(<SpikeProtectionProjects />, {organization});
  152. expect(await screen.findByText('project1')).toBeInTheDocument();
  153. const requestOptions = mockGet.mock.calls[0][1];
  154. expect(requestOptions.query).toEqual(
  155. expect.not.objectContaining({query: ' is_member:1'})
  156. );
  157. });
  158. it('renders all projects for owner', async () => {
  159. // even under closed membership
  160. organization.access = ['org:admin'];
  161. render(<SpikeProtectionProjects />, {organization});
  162. expect(await screen.findByText('project1')).toBeInTheDocument();
  163. const requestOptions = mockGet.mock.calls[0][1];
  164. expect(requestOptions.query).toEqual(
  165. expect.not.objectContaining({query: ' is_member:1'})
  166. );
  167. });
  168. it('renders toggles', async () => {
  169. const project = projects[0]!;
  170. render(<SpikeProtectionProjects />, {router});
  171. expect(mockGet).toHaveBeenCalled();
  172. const {toggle} = await validateComponents(project, false);
  173. await userEvent.click(toggle);
  174. await validateComponents(project, true);
  175. expect(mockPost).toHaveBeenCalled();
  176. });
  177. it('renders enabled/disabled toggles from team-roles', async () => {
  178. const projectDisabled = projects[0]!;
  179. const projectEnabled = projects[1]!;
  180. organization.access = [];
  181. render(<SpikeProtectionProjects />, {organization});
  182. const toggleDisabled = await screen.findByTestId(
  183. `${projectDisabled.slug}-spike-protection-toggle`
  184. );
  185. expect(toggleDisabled).toBeDisabled();
  186. const toggleEnabled = await screen.findByTestId(
  187. `${projectEnabled.slug}-spike-protection-toggle`
  188. );
  189. expect(toggleEnabled).toBeEnabled();
  190. });
  191. it('renders default value for toggles', async () => {
  192. const newProjects = [
  193. ProjectFixture({
  194. id: '1',
  195. slug: 'project1',
  196. options: {[SPIKE_PROTECTION_OPTION_DISABLED]: true},
  197. }),
  198. ProjectFixture({
  199. id: '2',
  200. slug: 'project2',
  201. options: {[SPIKE_PROTECTION_OPTION_DISABLED]: false},
  202. }),
  203. ];
  204. MockApiClient.clearMockResponses();
  205. MockApiClient.addMockResponse({
  206. url: `/organizations/${organization.slug}/projects/`,
  207. method: 'GET',
  208. body: [...newProjects],
  209. statusCode: 200,
  210. });
  211. MockApiClient.addMockResponse({
  212. url: `/organizations/org-slug/notifications/available-actions/`,
  213. method: 'GET',
  214. body: AvailableNotificationActionsFixture(),
  215. statusCode: 200,
  216. });
  217. MockApiClient.addMockResponse({
  218. url: `/organizations/org-slug/notifications/actions/`,
  219. match: [MockApiClient.matchQuery({triggerType: 'spike-protection'})],
  220. method: 'GET',
  221. body: [],
  222. statusCode: 200,
  223. });
  224. render(<SpikeProtectionProjects />, {router});
  225. await validateComponents(newProjects[0]!, false);
  226. await validateComponents(newProjects[1]!, true);
  227. });
  228. it('responds to successful toggles', async () => {
  229. const project = projects[0]!;
  230. render(<SpikeProtectionProjects />, {router});
  231. expect(mockGet).toHaveBeenCalled();
  232. const {toggle} = await validateComponents(project, false);
  233. await userEvent.click(toggle);
  234. expect(mockPost).toHaveBeenCalled();
  235. await validateComponents(project, true);
  236. // toggle off spike protection
  237. await userEvent.click(toggle);
  238. expect(mockDelete).toHaveBeenCalled();
  239. await validateComponents(project, false);
  240. });
  241. it('responds to unsuccessful toggle', async () => {
  242. const mockPostFailed = MockApiClient.addMockResponse({
  243. url: `/organizations/${organization.slug}/spike-protections/`,
  244. method: 'POST',
  245. statusCode: 403,
  246. });
  247. const project = projects[0]!;
  248. render(<SpikeProtectionProjects />, {router});
  249. expect(mockGet).toHaveBeenCalled();
  250. const {toggle} = await validateComponents(project, false);
  251. await userEvent.click(toggle);
  252. expect(mockPostFailed).toHaveBeenCalled();
  253. await validateComponents(project, false);
  254. });
  255. it('searches successfully', async () => {
  256. const projectSlug = projects[0]!.slug;
  257. render(<SpikeProtectionProjects />, {organization});
  258. const projectSearch = await screen.findByPlaceholderText('Search projects');
  259. await userEvent.click(projectSearch);
  260. await userEvent.paste(projectSlug);
  261. expect(projectSearch).toHaveValue(projectSlug);
  262. expect(mockGet).toHaveBeenCalledTimes(2);
  263. const requestOptions = mockGet.mock.calls[1][1];
  264. expect(requestOptions.query).toEqual(expect.objectContaining({query: projectSlug}));
  265. });
  266. it('response to successful toggle for all projects', async () => {
  267. renderGlobalModal();
  268. render(<SpikeProtectionProjects />, {router});
  269. await validateComponents(projects[0]!, false);
  270. await validateComponents(projects[1]!, true);
  271. const enableAll = await screen.findByTestId(`sp-enable-all`);
  272. await userEvent.click(enableAll);
  273. await userEvent.click(await screen.findByTestId('confirm-button'));
  274. expect(mockPostAll).toHaveBeenCalledWith(
  275. expect.anything(),
  276. expect.objectContaining({data: {projects: []}})
  277. );
  278. for (const project of projects) {
  279. await validateComponents(project, true);
  280. }
  281. });
  282. it('response to successful disable toggle for all projects', async () => {
  283. renderGlobalModal();
  284. render(<SpikeProtectionProjects />, {router});
  285. await validateComponents(projects[0]!, false);
  286. await validateComponents(projects[1]!, true);
  287. const disableAll = await screen.findByTestId(`sp-disable-all`);
  288. await userEvent.click(disableAll);
  289. await userEvent.click(await screen.findByTestId('confirm-button'));
  290. expect(mockDeleteAll).toHaveBeenCalledWith(
  291. expect.anything(),
  292. expect.objectContaining({data: {projects: []}})
  293. );
  294. for (const project of projects) {
  295. await validateComponents(project, false);
  296. }
  297. });
  298. it('fetches notification actions upon accordion opening', async () => {
  299. const project = projects[0]!;
  300. mockGet = MockApiClient.addMockResponse({
  301. url: `/organizations/${organization.slug}/projects/`,
  302. method: 'GET',
  303. body: [project],
  304. statusCode: 200,
  305. });
  306. render(<SpikeProtectionProjects />, {organization});
  307. await userEvent.click(
  308. await screen.findByTestId(`${project.slug}-spike-protection-toggle`)
  309. );
  310. await userEvent.click(screen.getByTestId('accordion-title'));
  311. expect(mockGetProjectNotificationActions).toHaveBeenCalled();
  312. expect(await screen.findByTestId('sentry_notification-action')).toBeInTheDocument();
  313. });
  314. it('closes the accordion upon disable', async () => {
  315. const project = projects[0]!;
  316. render(<SpikeProtectionProjects />, {organization});
  317. await userEvent.click(
  318. await screen.findByTestId(`${project.slug}-spike-protection-toggle`)
  319. );
  320. await userEvent.click(screen.getByTestId(`${project.slug}-spike-protection-toggle`));
  321. expect(
  322. screen.getByTestId(`${project.slug}-accordion-row-disabled`)
  323. ).toBeInTheDocument();
  324. });
  325. });