index.spec.tsx 13 KB

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