index.spec.tsx 12 KB

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