index.spec.tsx 12 KB

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