projectGeneralSettings.spec.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import {browserHistory} from 'react-router';
  2. import React from 'react';
  3. import {mount} from 'enzyme';
  4. import ProjectContext from 'app/views/projects/projectContext';
  5. import ProjectGeneralSettings from 'app/views/settings/projectGeneralSettings';
  6. import ProjectsStore from 'app/stores/projectsStore';
  7. import {selectByValue} from 'app-test/helpers/select';
  8. jest.mock('jquery');
  9. describe('projectGeneralSettings', function() {
  10. const org = TestStubs.Organization();
  11. const project = TestStubs.ProjectDetails();
  12. const groupingConfigs = TestStubs.GroupingConfigs();
  13. const groupingEnhancements = TestStubs.GroupingEnhancements();
  14. let routerContext;
  15. let putMock;
  16. beforeEach(function() {
  17. jest.spyOn(window.location, 'assign');
  18. routerContext = TestStubs.routerContext([
  19. {
  20. router: TestStubs.router({
  21. params: {
  22. projectId: project.slug,
  23. orgId: org.slug,
  24. },
  25. }),
  26. },
  27. ]);
  28. MockApiClient.clearMockResponses();
  29. MockApiClient.addMockResponse({
  30. url: '/grouping-configs/',
  31. method: 'GET',
  32. body: groupingConfigs,
  33. });
  34. MockApiClient.addMockResponse({
  35. url: '/grouping-enhancements/',
  36. method: 'GET',
  37. body: groupingEnhancements,
  38. });
  39. MockApiClient.addMockResponse({
  40. url: `/projects/${org.slug}/${project.slug}/`,
  41. method: 'GET',
  42. body: project,
  43. });
  44. MockApiClient.addMockResponse({
  45. url: `/projects/${org.slug}/${project.slug}/environments/`,
  46. method: 'GET',
  47. body: [],
  48. });
  49. MockApiClient.addMockResponse({
  50. url: `/organizations/${org.slug}/users/`,
  51. method: 'GET',
  52. body: [],
  53. });
  54. });
  55. afterEach(function() {
  56. window.location.assign.mockRestore();
  57. });
  58. it('renders form fields', function() {
  59. const wrapper = mount(
  60. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />,
  61. TestStubs.routerContext()
  62. );
  63. expect(wrapper.find('Input[name="slug"]').prop('value')).toBe('project-slug');
  64. expect(wrapper.find('Input[name="subjectPrefix"]').prop('value')).toBe('[my-org]');
  65. expect(wrapper.find('RangeSlider[name="resolveAge"]').prop('value')).toBe(48);
  66. expect(wrapper.find('Switch[name="dataScrubber"]').prop('isActive')).toBeFalsy();
  67. expect(
  68. wrapper.find('Switch[name="dataScrubberDefaults"]').prop('isActive')
  69. ).toBeFalsy();
  70. expect(wrapper.find('Switch[name="scrubIPAddresses"]').prop('isActive')).toBeFalsy();
  71. expect(wrapper.find('TextArea[name="sensitiveFields"]').prop('value')).toBe(
  72. 'creditcard\nssn'
  73. );
  74. expect(wrapper.find('TextArea[name="safeFields"]').prop('value')).toBe(
  75. 'business-email\ncompany'
  76. );
  77. expect(wrapper.find('TextArea[name="allowedDomains"]').prop('value')).toBe(
  78. 'example.com\nhttps://example.com'
  79. );
  80. expect(wrapper.find('Switch[name="scrapeJavaScript"]').prop('isDisabled')).toBe(
  81. false
  82. );
  83. expect(wrapper.find('Switch[name="scrapeJavaScript"]').prop('isActive')).toBeTruthy();
  84. expect(wrapper.find('Input[name="securityToken"]').prop('value')).toBe(
  85. 'security-token'
  86. );
  87. expect(wrapper.find('Input[name="securityTokenHeader"]').prop('value')).toBe(
  88. 'x-security-header'
  89. );
  90. expect(wrapper.find('Switch[name="verifySSL"]').prop('isActive')).toBeTruthy();
  91. });
  92. it('disables field when equivalent org setting is true', function() {
  93. routerContext.context.organization.dataScrubber = true;
  94. routerContext.context.organization.scrubIPAddresses = false;
  95. const wrapper = mount(
  96. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />,
  97. routerContext
  98. );
  99. expect(wrapper.find('Switch[name="scrubIPAddresses"]').prop('isDisabled')).toBe(
  100. false
  101. );
  102. expect(wrapper.find('Switch[name="scrubIPAddresses"]').prop('isActive')).toBeFalsy();
  103. expect(wrapper.find('Switch[name="dataScrubber"]').prop('isDisabled')).toBe(true);
  104. expect(wrapper.find('Switch[name="dataScrubber"]').prop('isActive')).toBe(true);
  105. });
  106. it('disables scrapeJavaScript when equivalent org setting is false', function() {
  107. routerContext.context.organization.scrapeJavaScript = false;
  108. const wrapper = mount(
  109. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />,
  110. routerContext
  111. );
  112. expect(wrapper.find('Switch[name="scrapeJavaScript"]').prop('isDisabled')).toBe(true);
  113. expect(wrapper.find('Switch[name="scrapeJavaScript"]').prop('isActive')).toBeFalsy();
  114. });
  115. it('project admins can remove project', function() {
  116. const deleteMock = MockApiClient.addMockResponse({
  117. url: `/projects/${org.slug}/${project.slug}/`,
  118. method: 'DELETE',
  119. });
  120. const wrapper = mount(
  121. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />,
  122. TestStubs.routerContext()
  123. );
  124. const removeBtn = wrapper.find('.ref-remove-project').first();
  125. expect(removeBtn.prop('children')).toBe('Remove Project');
  126. // Click button
  127. removeBtn.simulate('click');
  128. // Confirm Modal
  129. wrapper.find('Modal Button[priority="danger"]').simulate('click');
  130. expect(deleteMock).toHaveBeenCalled();
  131. });
  132. it('project admins can transfer project', function() {
  133. const deleteMock = MockApiClient.addMockResponse({
  134. url: `/projects/${org.slug}/${project.slug}/transfer/`,
  135. method: 'POST',
  136. });
  137. const wrapper = mount(
  138. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />,
  139. TestStubs.routerContext()
  140. );
  141. const removeBtn = wrapper.find('.ref-transfer-project').first();
  142. expect(removeBtn.prop('children')).toBe('Transfer Project');
  143. // Click button
  144. removeBtn.simulate('click');
  145. // Confirm Modal
  146. wrapper
  147. .find('input[name="email"]')
  148. .simulate('change', {target: {value: 'billy@sentry.io'}});
  149. wrapper.find('Modal Button[priority="danger"]').simulate('click');
  150. expect(deleteMock).toHaveBeenCalledWith(
  151. `/projects/${org.slug}/${project.slug}/transfer/`,
  152. expect.objectContaining({
  153. method: 'POST',
  154. data: {
  155. email: 'billy@sentry.io',
  156. },
  157. })
  158. );
  159. });
  160. it('displays transfer/remove message for non-admins', function() {
  161. routerContext.context.organization.access = ['org:read'];
  162. const wrapper = mount(
  163. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />,
  164. routerContext
  165. );
  166. expect(wrapper.html()).toContain(
  167. 'You do not have the required permission to remove this project.'
  168. );
  169. expect(wrapper.html()).toContain(
  170. 'You do not have the required permission to transfer this project.'
  171. );
  172. });
  173. it('disables the form for users without write permissions', function() {
  174. routerContext.context.organization.access = ['org:read'];
  175. const wrapper = mount(
  176. <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />,
  177. routerContext
  178. );
  179. expect(wrapper.find('FormField[disabled=false]')).toHaveLength(0);
  180. expect(
  181. wrapper
  182. .find('Alert')
  183. .first()
  184. .text()
  185. ).toBe(
  186. 'These settings can only be edited by users with the organization owner, manager, or admin role.'
  187. );
  188. });
  189. it('changing project platform updates ProjectsStore', async function() {
  190. const params = {orgId: org.slug, projectId: project.slug};
  191. ProjectsStore.loadInitialData([project]);
  192. putMock = MockApiClient.addMockResponse({
  193. url: `/projects/${org.slug}/${project.slug}/`,
  194. method: 'PUT',
  195. body: {
  196. ...project,
  197. platform: 'javascript',
  198. },
  199. });
  200. const wrapper = mount(
  201. <ProjectContext orgId={org.slug} projectId={project.slug}>
  202. <ProjectGeneralSettings
  203. routes={[]}
  204. location={routerContext.context.location}
  205. params={params}
  206. />
  207. </ProjectContext>,
  208. routerContext
  209. );
  210. await tick();
  211. wrapper.update();
  212. // Change slug to new-slug
  213. selectByValue(wrapper, 'javascript');
  214. // Slug does not save on blur
  215. expect(putMock).toHaveBeenCalled();
  216. await tick();
  217. await tick();
  218. wrapper.update();
  219. // updates ProjectsStore
  220. expect(ProjectsStore.itemsById['2'].platform).toBe('javascript');
  221. });
  222. it('changing slug updates ProjectsStore', async function() {
  223. const params = {orgId: org.slug, 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. const wrapper = mount(
  234. <ProjectContext orgId={org.slug} projectId={project.slug}>
  235. <ProjectGeneralSettings
  236. routes={[]}
  237. location={routerContext.context.location}
  238. params={params}
  239. />
  240. </ProjectContext>,
  241. routerContext
  242. );
  243. await tick();
  244. wrapper.update();
  245. // Change slug to new-slug
  246. wrapper
  247. .find('input[name="slug"]')
  248. .simulate('change', {target: {value: 'NEW PROJECT'}})
  249. .simulate('blur');
  250. // Slug does not save on blur
  251. expect(putMock).not.toHaveBeenCalled();
  252. wrapper.find('SaveButton').simulate('click');
  253. // fetches new slug
  254. const newProjectGet = MockApiClient.addMockResponse({
  255. url: `/projects/${org.slug}/new-project/`,
  256. method: 'GET',
  257. body: {...project, slug: 'new-project'},
  258. });
  259. const newProjectMembers = MockApiClient.addMockResponse({
  260. url: `/organizations/${org.slug}/users/`,
  261. method: 'GET',
  262. body: [],
  263. });
  264. await tick();
  265. // :(
  266. await tick();
  267. wrapper.update();
  268. // updates ProjectsStore
  269. expect(ProjectsStore.itemsById['2'].slug).toBe('new-project');
  270. expect(browserHistory.replace).toHaveBeenCalled();
  271. expect(wrapper.find('Input[name="slug"]').prop('value')).toBe('new-project');
  272. wrapper.setProps({
  273. projectId: 'new-project',
  274. });
  275. await tick();
  276. wrapper.update();
  277. expect(newProjectGet).toHaveBeenCalled();
  278. expect(newProjectMembers).toHaveBeenCalled();
  279. });
  280. describe('Non-"save on blur" Field', function() {
  281. let wrapper;
  282. beforeEach(function() {
  283. const params = {orgId: org.slug, projectId: project.slug};
  284. ProjectsStore.loadInitialData([project]);
  285. putMock = MockApiClient.addMockResponse({
  286. url: `/projects/${org.slug}/${project.slug}/`,
  287. method: 'PUT',
  288. body: {
  289. ...project,
  290. slug: 'new-project',
  291. },
  292. });
  293. wrapper = mount(
  294. <ProjectContext orgId={org.slug} projectId={project.slug}>
  295. <ProjectGeneralSettings
  296. routes={[]}
  297. location={routerContext.context.location}
  298. params={params}
  299. />
  300. </ProjectContext>,
  301. routerContext
  302. );
  303. });
  304. it('can cancel unsaved changes for a field', async function() {
  305. await tick();
  306. wrapper.update();
  307. // Initially does not have "Cancel" button
  308. expect(wrapper.find('MessageAndActions CancelButton')).toHaveLength(0);
  309. // Has initial value
  310. expect(wrapper.find('input[name="resolveAge"]').prop('value')).toBe(19);
  311. // Change value
  312. wrapper
  313. .find('input[name="resolveAge"]')
  314. .simulate('input', {target: {value: 12}})
  315. .simulate('mouseUp');
  316. // Has updated value
  317. expect(wrapper.find('input[name="resolveAge"]').prop('value')).toBe(12);
  318. // Has "Cancel" button visible
  319. expect(wrapper.find('MessageAndActions CancelButton')).toHaveLength(1);
  320. // Click cancel
  321. wrapper.find('MessageAndActions CancelButton').simulate('click');
  322. // Cancel row should disappear
  323. expect(wrapper.find('MessageAndActions CancelButton')).toHaveLength(0);
  324. // Value should be reverted
  325. expect(wrapper.find('input[name="resolveAge"]').prop('value')).toBe(19);
  326. // PUT should not be called
  327. expect(putMock).not.toHaveBeenCalled();
  328. });
  329. it('saves when value is changed and "Save" clicked', async function() {
  330. await tick();
  331. wrapper.update();
  332. // Initially does not have "Save" button
  333. expect(wrapper.find('MessageAndActions SaveButton')).toHaveLength(0);
  334. // Change value
  335. wrapper
  336. .find('input[name="resolveAge"]')
  337. .simulate('input', {target: {value: 12}})
  338. .simulate('mouseUp');
  339. // Has "Save" button visible
  340. expect(wrapper.find('MessageAndActions SaveButton')).toHaveLength(1);
  341. // Should not have put mock called yet
  342. expect(putMock).not.toHaveBeenCalled();
  343. // Click "Save"
  344. wrapper.find('MessageAndActions SaveButton').simulate('click');
  345. // API endpoint should have been called
  346. expect(putMock).toHaveBeenCalledWith(
  347. expect.anything(),
  348. expect.objectContaining({
  349. data: {
  350. resolveAge: 12,
  351. },
  352. })
  353. );
  354. // Should hide "Save" button after saving
  355. await tick();
  356. wrapper.update();
  357. expect(wrapper.find('MessageAndActions SaveButton')).toHaveLength(0);
  358. });
  359. });
  360. });