index.spec.tsx 12 KB

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