projectGeneralSettings.spec.jsx 13 KB

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