accountSecurity.spec.jsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {mountGlobalModal} from 'sentry-test/modal';
  3. import {Client} from 'sentry/api';
  4. import ModalStore from 'sentry/stores/modalStore';
  5. import AccountSecurity from 'sentry/views/settings/account/accountSecurity';
  6. import AccountSecurityWrapper from 'sentry/views/settings/account/accountSecurity/accountSecurityWrapper';
  7. const ENDPOINT = '/users/me/authenticators/';
  8. const ORG_ENDPOINT = '/organizations/';
  9. const ACCOUNT_EMAILS_ENDPOINT = '/users/me/emails/';
  10. const AUTH_ENDPOINT = '/auth/';
  11. describe('AccountSecurity', function () {
  12. beforeEach(function () {
  13. jest.spyOn(window.location, 'assign').mockImplementation(() => {});
  14. Client.clearMockResponses();
  15. Client.addMockResponse({
  16. url: ORG_ENDPOINT,
  17. body: TestStubs.Organizations(),
  18. });
  19. Client.addMockResponse({
  20. url: ACCOUNT_EMAILS_ENDPOINT,
  21. body: TestStubs.AccountEmails(),
  22. });
  23. });
  24. afterEach(function () {
  25. window.location.assign.mockRestore();
  26. });
  27. it('renders empty', function () {
  28. Client.addMockResponse({
  29. url: ENDPOINT,
  30. body: [],
  31. });
  32. const wrapper = mountWithTheme(
  33. <AccountSecurityWrapper>
  34. <AccountSecurity />
  35. </AccountSecurityWrapper>,
  36. TestStubs.routerContext()
  37. );
  38. expect(wrapper.find('EmptyMessage')).toHaveLength(1);
  39. expect(wrapper.find('TwoFactorRequired')).toHaveLength(0);
  40. });
  41. it('renders a primary interface that is enrolled', function () {
  42. Client.addMockResponse({
  43. url: ENDPOINT,
  44. body: [TestStubs.Authenticators().Totp({configureButton: 'Info'})],
  45. });
  46. const wrapper = mountWithTheme(
  47. <AccountSecurityWrapper>
  48. <AccountSecurity />
  49. </AccountSecurityWrapper>,
  50. TestStubs.routerContext()
  51. );
  52. expect(wrapper.find('AuthenticatorName').prop('children')).toBe('Authenticator App');
  53. // There should be an "Info" button
  54. expect(
  55. wrapper.find('Button[className="details-button"]').first().prop('children')
  56. ).toBe('Info');
  57. // Remove button
  58. expect(wrapper.find('button[aria-label="delete"]')).toHaveLength(1);
  59. expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true);
  60. expect(wrapper.find('TwoFactorRequired')).toHaveLength(0);
  61. });
  62. it('can delete enrolled authenticator', async function () {
  63. Client.addMockResponse({
  64. url: ENDPOINT,
  65. body: [
  66. TestStubs.Authenticators().Totp({
  67. authId: '15',
  68. configureButton: 'Info',
  69. }),
  70. ],
  71. });
  72. const deleteMock = Client.addMockResponse({
  73. url: `${ENDPOINT}15/`,
  74. method: 'DELETE',
  75. });
  76. expect(deleteMock).not.toHaveBeenCalled();
  77. const wrapper = mountWithTheme(
  78. <AccountSecurityWrapper>
  79. <AccountSecurity />
  80. </AccountSecurityWrapper>,
  81. TestStubs.routerContext()
  82. );
  83. expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true);
  84. // next authenticators request should have totp disabled
  85. const authenticatorsMock = Client.addMockResponse({
  86. url: ENDPOINT,
  87. body: [
  88. TestStubs.Authenticators().Totp({
  89. isEnrolled: false,
  90. authId: '15',
  91. configureButton: 'Info',
  92. }),
  93. ],
  94. });
  95. // This will open confirm modal
  96. wrapper.find('button[aria-label="delete"]').simulate('click');
  97. // Confirm
  98. const modal = await mountGlobalModal();
  99. modal.find('Button').last().simulate('click');
  100. await tick();
  101. wrapper.update();
  102. expect(deleteMock).toHaveBeenCalled();
  103. // Should only have been called once
  104. expect(authenticatorsMock).toHaveBeenCalledTimes(1);
  105. expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(false);
  106. // No enrolled authenticators
  107. expect(wrapper.find('TwoFactorRequired')).toHaveLength(1);
  108. });
  109. it('can remove one of multiple 2fa methods when org requires 2fa', async function () {
  110. Client.addMockResponse({
  111. url: ENDPOINT,
  112. body: [
  113. TestStubs.Authenticators().Totp({
  114. authId: '15',
  115. configureButton: 'Info',
  116. }),
  117. TestStubs.Authenticators().U2f(),
  118. ],
  119. });
  120. Client.addMockResponse({
  121. url: ORG_ENDPOINT,
  122. body: TestStubs.Organizations({require2FA: true}),
  123. });
  124. const deleteMock = Client.addMockResponse({
  125. url: `${ENDPOINT}15/`,
  126. method: 'DELETE',
  127. });
  128. expect(deleteMock).not.toHaveBeenCalled();
  129. const wrapper = mountWithTheme(
  130. <AccountSecurityWrapper>
  131. <AccountSecurity />
  132. </AccountSecurityWrapper>,
  133. TestStubs.routerContext()
  134. );
  135. expect(wrapper.find('AuthenticatorStatus').first().prop('enabled')).toBe(true);
  136. expect(wrapper.find('RemoveConfirm').first().prop('disabled')).toBe(false);
  137. expect(wrapper.find('Tooltip').first().prop('disabled')).toBe(true);
  138. // This will open confirm modal
  139. wrapper.find('button[aria-label="delete"]').first().simulate('click');
  140. // Confirm
  141. const modal = await mountGlobalModal();
  142. modal.find('Button').last().simulate('click');
  143. expect(deleteMock).toHaveBeenCalled();
  144. });
  145. it('can not remove last 2fa method when org requires 2fa', function () {
  146. Client.addMockResponse({
  147. url: ENDPOINT,
  148. body: [
  149. TestStubs.Authenticators().Totp({
  150. authId: '15',
  151. configureButton: 'Info',
  152. }),
  153. ],
  154. });
  155. Client.addMockResponse({
  156. url: ORG_ENDPOINT,
  157. body: TestStubs.Organizations({require2FA: true}),
  158. });
  159. const deleteMock = Client.addMockResponse({
  160. url: `${ENDPOINT}15/`,
  161. method: 'DELETE',
  162. });
  163. expect(deleteMock).not.toHaveBeenCalled();
  164. const wrapper = mountWithTheme(
  165. <AccountSecurityWrapper>
  166. <AccountSecurity />
  167. </AccountSecurityWrapper>,
  168. TestStubs.routerContext()
  169. );
  170. expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true);
  171. expect(wrapper.find('RemoveConfirm').prop('disabled')).toBe(true);
  172. expect(wrapper.find('Tooltip').prop('disabled')).toBe(false);
  173. expect(wrapper.find('Tooltip').prop('title')).toContain('test 1 and test 2');
  174. // This will open confirm modal
  175. wrapper.find('button[aria-label="delete"]').simulate('click');
  176. // Confirm
  177. expect(wrapper.find('Modal Button')).toHaveLength(0);
  178. expect(deleteMock).not.toHaveBeenCalled();
  179. });
  180. it('cannot enroll without verified email', async function () {
  181. Client.addMockResponse({
  182. url: ENDPOINT,
  183. body: [TestStubs.Authenticators().Totp({isEnrolled: false})],
  184. });
  185. Client.addMockResponse({
  186. url: ACCOUNT_EMAILS_ENDPOINT,
  187. body: [
  188. {
  189. email: 'primary@example.com',
  190. isPrimary: true,
  191. isVerified: false,
  192. },
  193. ],
  194. });
  195. const wrapper = mountWithTheme(
  196. <AccountSecurityWrapper>
  197. <AccountSecurity />
  198. </AccountSecurityWrapper>,
  199. TestStubs.routerContext()
  200. );
  201. expect(wrapper.find('AuthenticatorName').prop('children')).toBe('Authenticator App');
  202. // There should be an "Add" button
  203. expect(
  204. wrapper.find('Button[className="enroll-button"]').first().prop('children')
  205. ).toBe('Add');
  206. expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(false);
  207. // user is not 2fa enrolled
  208. expect(wrapper.find('TwoFactorRequired')).toHaveLength(1);
  209. // expect modal to be called
  210. const openEmailModalFunc = jest.spyOn(ModalStore, 'openModal');
  211. const Add2FAButton = wrapper.find('Button[className="enroll-button"]').first();
  212. Add2FAButton.simulate('click');
  213. await tick();
  214. expect(openEmailModalFunc).toHaveBeenCalled();
  215. });
  216. it('renders a backup interface that is not enrolled', function () {
  217. Client.addMockResponse({
  218. url: ENDPOINT,
  219. body: [TestStubs.Authenticators().Recovery({isEnrolled: false})],
  220. });
  221. const wrapper = mountWithTheme(
  222. <AccountSecurityWrapper>
  223. <AccountSecurity />
  224. </AccountSecurityWrapper>,
  225. TestStubs.routerContext()
  226. );
  227. expect(wrapper.find('AuthenticatorName').prop('children')).toBe('Recovery Codes');
  228. // There should be an View Codes button
  229. expect(wrapper.find('Button[className="details-button"]')).toHaveLength(0);
  230. expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(false);
  231. // user is not 2fa enrolled
  232. expect(wrapper.find('TwoFactorRequired')).toHaveLength(1);
  233. });
  234. it('renders a primary interface that is not enrolled', function () {
  235. Client.addMockResponse({
  236. url: ENDPOINT,
  237. body: [TestStubs.Authenticators().Totp({isEnrolled: false})],
  238. });
  239. const wrapper = mountWithTheme(
  240. <AccountSecurityWrapper>
  241. <AccountSecurity />
  242. </AccountSecurityWrapper>,
  243. TestStubs.routerContext()
  244. );
  245. expect(wrapper.find('AuthenticatorName').prop('children')).toBe('Authenticator App');
  246. // There should be an "Add" button
  247. expect(
  248. wrapper.find('Button[className="enroll-button"]').first().prop('children')
  249. ).toBe('Add');
  250. expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(false);
  251. // user is not 2fa enrolled
  252. expect(wrapper.find('TwoFactorRequired')).toHaveLength(1);
  253. });
  254. it('does not render primary interface that disallows new enrollments', function () {
  255. Client.addMockResponse({
  256. url: ENDPOINT,
  257. body: [
  258. TestStubs.Authenticators().Totp({disallowNewEnrollment: false}),
  259. TestStubs.Authenticators().U2f({disallowNewEnrollment: null}),
  260. TestStubs.Authenticators().Sms({disallowNewEnrollment: true}),
  261. ],
  262. });
  263. const wrapper = mountWithTheme(
  264. <AccountSecurityWrapper>
  265. <AccountSecurity />
  266. </AccountSecurityWrapper>,
  267. TestStubs.routerContext()
  268. );
  269. // There should only be two authenticators rendered
  270. expect(wrapper.find('AuthenticatorName')).toHaveLength(2);
  271. });
  272. it('renders primary interface if new enrollments are disallowed, but we are enrolled', function () {
  273. Client.addMockResponse({
  274. url: ENDPOINT,
  275. body: [
  276. TestStubs.Authenticators().Sms({isEnrolled: true, disallowNewEnrollment: true}),
  277. ],
  278. });
  279. const wrapper = mountWithTheme(
  280. <AccountSecurityWrapper>
  281. <AccountSecurity />
  282. </AccountSecurityWrapper>,
  283. TestStubs.routerContext()
  284. );
  285. // Should still render the authenticator since we are already enrolled
  286. expect(wrapper.find('AuthenticatorName')).toHaveLength(1);
  287. expect(wrapper.find('AuthenticatorName').prop('children')).toBe('Text Message');
  288. });
  289. it('renders a backup interface that is enrolled', function () {
  290. Client.addMockResponse({
  291. url: ENDPOINT,
  292. body: [TestStubs.Authenticators().Recovery({isEnrolled: true})],
  293. });
  294. const wrapper = mountWithTheme(
  295. <AccountSecurityWrapper>
  296. <AccountSecurity />
  297. </AccountSecurityWrapper>,
  298. TestStubs.routerContext()
  299. );
  300. expect(wrapper.find('AuthenticatorName').prop('children')).toBe('Recovery Codes');
  301. // There should be an View Codes button
  302. expect(
  303. wrapper.find('Button[className="details-button"]').first().prop('children')
  304. ).toBe('View Codes');
  305. expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true);
  306. });
  307. it('can change password', function () {
  308. Client.addMockResponse({
  309. url: ENDPOINT,
  310. body: [TestStubs.Authenticators().Recovery({isEnrolled: false})],
  311. });
  312. const url = '/users/me/password/';
  313. const mock = Client.addMockResponse({
  314. url,
  315. method: 'PUT',
  316. });
  317. const wrapper = mountWithTheme(
  318. <AccountSecurityWrapper>
  319. <AccountSecurity />
  320. </AccountSecurityWrapper>,
  321. TestStubs.routerContext()
  322. );
  323. wrapper
  324. .find('PasswordForm input[name="password"]')
  325. .simulate('change', {target: {value: 'oldpassword'}});
  326. wrapper
  327. .find('PasswordForm input[name="passwordNew"]')
  328. .simulate('change', {target: {value: 'newpassword'}});
  329. wrapper
  330. .find('PasswordForm input[name="passwordVerify"]')
  331. .simulate('change', {target: {value: 'newpassword'}});
  332. wrapper.find('PasswordForm form').simulate('submit');
  333. expect(mock).toHaveBeenCalledWith(
  334. url,
  335. expect.objectContaining({
  336. method: 'PUT',
  337. data: {
  338. password: 'oldpassword',
  339. passwordNew: 'newpassword',
  340. passwordVerify: 'newpassword',
  341. },
  342. })
  343. );
  344. // user is not 2fa enrolled
  345. expect(wrapper.find('TwoFactorRequired')).toHaveLength(1);
  346. });
  347. it('requires current password to be entered', function () {
  348. Client.addMockResponse({
  349. url: ENDPOINT,
  350. body: [TestStubs.Authenticators().Recovery({isEnrolled: false})],
  351. });
  352. const url = '/users/me/password/';
  353. const mock = Client.addMockResponse({
  354. url,
  355. method: 'PUT',
  356. });
  357. const wrapper = mountWithTheme(
  358. <AccountSecurityWrapper>
  359. <AccountSecurity />
  360. </AccountSecurityWrapper>,
  361. TestStubs.routerContext()
  362. );
  363. wrapper
  364. .find('PasswordForm input[name="passwordNew"]')
  365. .simulate('change', {target: {value: 'newpassword'}});
  366. wrapper
  367. .find('PasswordForm input[name="passwordVerify"]')
  368. .simulate('change', {target: {value: 'newpassword'}});
  369. wrapper.find('PasswordForm form').simulate('submit');
  370. expect(mock).not.toHaveBeenCalled();
  371. // user is not 2fa enrolled
  372. expect(wrapper.find('TwoFactorRequired')).toHaveLength(1);
  373. });
  374. it('can expire all sessions', async function () {
  375. Client.addMockResponse({
  376. url: ENDPOINT,
  377. body: [TestStubs.Authenticators().Recovery({isEnrolled: false})],
  378. });
  379. const mock = Client.addMockResponse({
  380. url: AUTH_ENDPOINT,
  381. body: {all: true},
  382. method: 'DELETE',
  383. status: 204,
  384. });
  385. const wrapper = mountWithTheme(
  386. <AccountSecurityWrapper>
  387. <AccountSecurity />
  388. </AccountSecurityWrapper>,
  389. TestStubs.routerContext()
  390. );
  391. wrapper.find('Button[data-test-id="signoutAll"]').simulate('click');
  392. await tick();
  393. expect(window.location.assign).toHaveBeenCalledWith('/auth/login/');
  394. expect(mock).toHaveBeenCalled();
  395. });
  396. });