index.spec.tsx 12 KB

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