index.spec.jsx 12 KB

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