index.spec.tsx 12 KB

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