projectGeneralSettings.spec.jsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. import {browserHistory} from 'react-router';
  2. import selectEvent from 'react-select-event';
  3. import {
  4. act,
  5. fireEvent,
  6. render,
  7. renderGlobalModal,
  8. screen,
  9. userEvent,
  10. waitFor,
  11. } from 'sentry-test/reactTestingLibrary';
  12. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  13. import {removePageFiltersStorage} from 'sentry/components/organizations/pageFilters/persistence';
  14. import ProjectsStore from 'sentry/stores/projectsStore';
  15. import ProjectContext from 'sentry/views/projects/projectContext';
  16. import ProjectGeneralSettings from 'sentry/views/settings/projectGeneralSettings';
  17. jest.mock('sentry/actionCreators/indicator');
  18. jest.mock('sentry/components/organizations/pageFilters/persistence');
  19. function getField(role, name) {
  20. return screen.getByRole(role, {name});
  21. }
  22. describe('projectGeneralSettings', function () {
  23. const org = TestStubs.Organization();
  24. const project = TestStubs.ProjectDetails();
  25. const groupingConfigs = TestStubs.GroupingConfigs();
  26. const groupingEnhancements = TestStubs.GroupingEnhancements();
  27. let routerContext;
  28. let putMock;
  29. beforeEach(function () {
  30. jest.spyOn(window.location, 'assign');
  31. routerContext = TestStubs.routerContext([
  32. {
  33. router: TestStubs.router({
  34. params: {
  35. projectId: project.slug,
  36. orgId: org.slug,
  37. },
  38. }),
  39. },
  40. ]);
  41. MockApiClient.clearMockResponses();
  42. MockApiClient.addMockResponse({
  43. url: '/grouping-configs/',
  44. method: 'GET',
  45. body: groupingConfigs,
  46. });
  47. MockApiClient.addMockResponse({
  48. url: '/grouping-enhancements/',
  49. method: 'GET',
  50. body: groupingEnhancements,
  51. });
  52. MockApiClient.addMockResponse({
  53. url: `/projects/${org.slug}/${project.slug}/`,
  54. method: 'GET',
  55. body: project,
  56. });
  57. MockApiClient.addMockResponse({
  58. url: `/projects/${org.slug}/${project.slug}/environments/`,
  59. method: 'GET',
  60. body: [],
  61. });
  62. MockApiClient.addMockResponse({
  63. url: `/organizations/${org.slug}/users/`,
  64. method: 'GET',
  65. body: [],
  66. });
  67. });
  68. afterEach(function () {
  69. window.location.assign.mockRestore();
  70. MockApiClient.clearMockResponses();
  71. addSuccessMessage.mockReset();
  72. addErrorMessage.mockReset();
  73. });
  74. it('renders form fields', function () {
  75. render(
  76. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />
  77. );
  78. expect(getField('textbox', 'Name')).toHaveValue('Project Name');
  79. expect(getField('textbox', 'Subject Prefix')).toHaveValue('[my-org]');
  80. // Step 19 of the auto resolve slider equates to 48 hours. This is
  81. // different from thee actual field value (which will be 48)
  82. expect(getField('slider', 'Auto Resolve')).toHaveValue('19');
  83. expect(getField('textbox', 'Allowed Domains')).toHaveValue(
  84. 'example.com\nhttps://example.com'
  85. );
  86. expect(getField('checkbox', 'Enable JavaScript source fetching')).toBeChecked();
  87. expect(getField('textbox', 'Security Token')).toHaveValue('security-token');
  88. expect(getField('textbox', 'Security Token Header')).toHaveValue('x-security-header');
  89. expect(getField('checkbox', 'Verify TLS/SSL')).toBeChecked();
  90. });
  91. it('disables scrapeJavaScript when equivalent org setting is false', function () {
  92. routerContext.context.organization.scrapeJavaScript = false;
  93. render(
  94. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />,
  95. {context: routerContext}
  96. );
  97. expect(getField('checkbox', 'Enable JavaScript source fetching')).toBeDisabled();
  98. expect(getField('checkbox', 'Enable JavaScript source fetching')).not.toBeChecked();
  99. });
  100. it('project admins can remove project', function () {
  101. const deleteMock = MockApiClient.addMockResponse({
  102. url: `/projects/${org.slug}/${project.slug}/`,
  103. method: 'DELETE',
  104. });
  105. render(
  106. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />
  107. );
  108. userEvent.click(screen.getByRole('button', {name: 'Remove Project'}));
  109. // Click confirmation button
  110. renderGlobalModal();
  111. userEvent.click(screen.getByTestId('confirm-button'));
  112. expect(deleteMock).toHaveBeenCalled();
  113. expect(removePageFiltersStorage).toHaveBeenCalledWith('org-slug');
  114. });
  115. it('project admins can transfer project', async function () {
  116. const deleteMock = MockApiClient.addMockResponse({
  117. url: `/projects/${org.slug}/${project.slug}/transfer/`,
  118. method: 'POST',
  119. });
  120. render(
  121. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />
  122. );
  123. userEvent.click(screen.getByRole('button', {name: 'Transfer Project'}));
  124. // Click confirmation button
  125. renderGlobalModal();
  126. userEvent.type(getField('textbox', 'Organization Owner'), 'billy@sentry.io');
  127. userEvent.click(screen.getByTestId('confirm-button'));
  128. await waitFor(() =>
  129. expect(deleteMock).toHaveBeenCalledWith(
  130. `/projects/${org.slug}/${project.slug}/transfer/`,
  131. expect.objectContaining({
  132. method: 'POST',
  133. data: {
  134. email: 'billy@sentry.io',
  135. },
  136. })
  137. )
  138. );
  139. expect(addSuccessMessage).toHaveBeenCalled();
  140. });
  141. it('handles errors on transfer project', async function () {
  142. const deleteMock = MockApiClient.addMockResponse({
  143. url: `/projects/${org.slug}/${project.slug}/transfer/`,
  144. method: 'POST',
  145. statusCode: 400,
  146. body: {detail: 'An organization owner could not be found'},
  147. });
  148. render(
  149. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />
  150. );
  151. userEvent.click(screen.getByRole('button', {name: 'Transfer Project'}));
  152. // Click confirmation button
  153. renderGlobalModal();
  154. userEvent.type(getField('textbox', 'Organization Owner'), 'billy@sentry.io');
  155. userEvent.click(screen.getByTestId('confirm-button'));
  156. await waitFor(() => expect(deleteMock).toHaveBeenCalled());
  157. expect(addSuccessMessage).not.toHaveBeenCalled();
  158. expect(addErrorMessage).toHaveBeenCalled();
  159. // Check the error message
  160. const {container} = render(addErrorMessage.mock.calls[0][0]);
  161. expect(container).toHaveTextContent(
  162. 'Error transferring project-slug. An organization owner could not be found'
  163. );
  164. });
  165. it('displays transfer/remove message for non-admins', function () {
  166. routerContext.context.organization.access = ['org:read'];
  167. const {container} = render(
  168. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />,
  169. {context: routerContext}
  170. );
  171. expect(container).toHaveTextContent(
  172. 'You do not have the required permission to remove this project.'
  173. );
  174. expect(container).toHaveTextContent(
  175. 'You do not have the required permission to transfer this project.'
  176. );
  177. });
  178. it('disables the form for users without write permissions', function () {
  179. routerContext.context.organization.access = ['org:read'];
  180. render(
  181. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />,
  182. {context: routerContext}
  183. );
  184. // no textboxes are enabled
  185. screen.queryAllByRole('textbox').forEach(textbox => expect(textbox).toBeDisabled());
  186. expect(screen.getByTestId('project-permission-alert')).toHaveTextContent(
  187. 'These settings can only be edited by users with the organization owner, manager, or admin role.'
  188. );
  189. });
  190. it('changing project platform updates ProjectsStore', async function () {
  191. const params = {orgId: org.slug, projectId: project.slug};
  192. ProjectsStore.loadInitialData([project]);
  193. putMock = MockApiClient.addMockResponse({
  194. url: `/projects/${org.slug}/${project.slug}/`,
  195. method: 'PUT',
  196. body: {
  197. ...project,
  198. platform: 'javascript',
  199. },
  200. });
  201. render(
  202. <ProjectContext orgId={org.slug} projectId={project.slug}>
  203. <ProjectGeneralSettings
  204. routes={[]}
  205. location={routerContext.context.location}
  206. params={params}
  207. />
  208. </ProjectContext>,
  209. {context: routerContext}
  210. );
  211. const platformSelect = await screen.findByRole('textbox', {name: 'Platform'});
  212. await selectEvent.select(platformSelect, ['React']);
  213. expect(putMock).toHaveBeenCalled();
  214. // updates ProjectsStore
  215. expect(ProjectsStore.itemsById['2'].platform).toBe('javascript');
  216. });
  217. it('changing name updates ProjectsStore', async function () {
  218. const params = {orgId: org.slug, projectId: project.slug};
  219. ProjectsStore.loadInitialData([project]);
  220. putMock = MockApiClient.addMockResponse({
  221. url: `/projects/${org.slug}/${project.slug}/`,
  222. method: 'PUT',
  223. body: {
  224. ...project,
  225. slug: 'new-project',
  226. },
  227. });
  228. render(
  229. <ProjectContext orgId={org.slug} projectId={project.slug}>
  230. <ProjectGeneralSettings
  231. routes={[]}
  232. location={routerContext.context.location}
  233. params={params}
  234. />
  235. </ProjectContext>,
  236. {context: routerContext}
  237. );
  238. userEvent.type(await screen.findByRole('textbox', {name: 'Name'}), 'New Project');
  239. // Slug does not save on blur
  240. expect(putMock).not.toHaveBeenCalled();
  241. // Saves when clicking save
  242. userEvent.click(screen.getByRole('button', {name: 'Save'}));
  243. // Redirects the user
  244. await waitFor(() => expect(browserHistory.replace).toHaveBeenCalled());
  245. expect(ProjectsStore.itemsById['2'].slug).toBe('new-project');
  246. });
  247. describe('Non-"save on blur" Field', function () {
  248. beforeEach(function () {
  249. const params = {orgId: org.slug, projectId: project.slug};
  250. ProjectsStore.loadInitialData([project]);
  251. putMock = MockApiClient.addMockResponse({
  252. url: `/projects/${org.slug}/${project.slug}/`,
  253. method: 'PUT',
  254. body: {
  255. ...project,
  256. slug: 'new-project',
  257. },
  258. });
  259. render(
  260. <ProjectContext orgId={org.slug} projectId={project.slug}>
  261. <ProjectGeneralSettings
  262. routes={[]}
  263. location={routerContext.context.location}
  264. params={params}
  265. />
  266. </ProjectContext>,
  267. {context: routerContext}
  268. );
  269. });
  270. it('can cancel unsaved changes for a field', async function () {
  271. expect(screen.queryByRole('button', {name: 'Cancel'})).not.toBeInTheDocument();
  272. const autoResolveSlider = getField('slider', 'Auto Resolve');
  273. expect(autoResolveSlider).toHaveValue('19');
  274. // Change value
  275. fireEvent.change(autoResolveSlider, {target: {value: '12'}});
  276. expect(autoResolveSlider).toHaveValue('12');
  277. // Click cancel
  278. userEvent.click(screen.getByRole('button', {name: 'Cancel'}));
  279. await act(tick);
  280. // Cancel row should disappear
  281. expect(screen.queryByRole('button', {name: 'Cancel'})).not.toBeInTheDocument();
  282. // Value should be reverted
  283. expect(autoResolveSlider).toHaveValue('19');
  284. // PUT should not be called
  285. expect(putMock).not.toHaveBeenCalled();
  286. });
  287. it('saves when value is changed and "Save" clicked', async function () {
  288. expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
  289. const autoResolveSlider = getField('slider', 'Auto Resolve');
  290. expect(autoResolveSlider).toHaveValue('19');
  291. // Change value
  292. fireEvent.change(autoResolveSlider, {target: {value: '12'}});
  293. expect(autoResolveSlider).toHaveValue('12');
  294. // Should not have put mock called yet
  295. expect(putMock).not.toHaveBeenCalled();
  296. // Click "Save"
  297. userEvent.click(screen.queryByRole('button', {name: 'Save'}));
  298. await act(tick);
  299. // API endpoint should have been called
  300. expect(putMock).toHaveBeenCalledWith(
  301. expect.anything(),
  302. expect.objectContaining({
  303. data: {
  304. resolveAge: 12,
  305. },
  306. })
  307. );
  308. expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
  309. });
  310. });
  311. });