accountSecurity.spec.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {mountGlobalModal} from 'sentry-test/modal';
  3. import ModalActions from 'sentry/actions/modalActions';
  4. import {Client} from 'sentry/api';
  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(ModalActions, '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('renders a backup interface that is enrolled', function () {
  255. Client.addMockResponse({
  256. url: ENDPOINT,
  257. body: [TestStubs.Authenticators().Recovery({isEnrolled: true})],
  258. });
  259. const wrapper = mountWithTheme(
  260. <AccountSecurityWrapper>
  261. <AccountSecurity />
  262. </AccountSecurityWrapper>,
  263. TestStubs.routerContext()
  264. );
  265. expect(wrapper.find('AuthenticatorName').prop('children')).toBe('Recovery Codes');
  266. // There should be an View Codes button
  267. expect(
  268. wrapper.find('Button[className="details-button"]').first().prop('children')
  269. ).toBe('View Codes');
  270. expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true);
  271. });
  272. it('can change password', function () {
  273. Client.addMockResponse({
  274. url: ENDPOINT,
  275. body: [TestStubs.Authenticators().Recovery({isEnrolled: false})],
  276. });
  277. const url = '/users/me/password/';
  278. const mock = Client.addMockResponse({
  279. url,
  280. method: 'PUT',
  281. });
  282. const wrapper = mountWithTheme(
  283. <AccountSecurityWrapper>
  284. <AccountSecurity />
  285. </AccountSecurityWrapper>,
  286. TestStubs.routerContext()
  287. );
  288. wrapper
  289. .find('PasswordForm input[name="password"]')
  290. .simulate('change', {target: {value: 'oldpassword'}});
  291. wrapper
  292. .find('PasswordForm input[name="passwordNew"]')
  293. .simulate('change', {target: {value: 'newpassword'}});
  294. wrapper
  295. .find('PasswordForm input[name="passwordVerify"]')
  296. .simulate('change', {target: {value: 'newpassword'}});
  297. wrapper.find('PasswordForm form').simulate('submit');
  298. expect(mock).toHaveBeenCalledWith(
  299. url,
  300. expect.objectContaining({
  301. method: 'PUT',
  302. data: {
  303. password: 'oldpassword',
  304. passwordNew: 'newpassword',
  305. passwordVerify: 'newpassword',
  306. },
  307. })
  308. );
  309. // user is not 2fa enrolled
  310. expect(wrapper.find('TwoFactorRequired')).toHaveLength(1);
  311. });
  312. it('requires current password to be entered', function () {
  313. Client.addMockResponse({
  314. url: ENDPOINT,
  315. body: [TestStubs.Authenticators().Recovery({isEnrolled: false})],
  316. });
  317. const url = '/users/me/password/';
  318. const mock = Client.addMockResponse({
  319. url,
  320. method: 'PUT',
  321. });
  322. const wrapper = mountWithTheme(
  323. <AccountSecurityWrapper>
  324. <AccountSecurity />
  325. </AccountSecurityWrapper>,
  326. TestStubs.routerContext()
  327. );
  328. wrapper
  329. .find('PasswordForm input[name="passwordNew"]')
  330. .simulate('change', {target: {value: 'newpassword'}});
  331. wrapper
  332. .find('PasswordForm input[name="passwordVerify"]')
  333. .simulate('change', {target: {value: 'newpassword'}});
  334. wrapper.find('PasswordForm form').simulate('submit');
  335. expect(mock).not.toHaveBeenCalled();
  336. // user is not 2fa enrolled
  337. expect(wrapper.find('TwoFactorRequired')).toHaveLength(1);
  338. });
  339. it('can expire all sessions', async function () {
  340. Client.addMockResponse({
  341. url: ENDPOINT,
  342. body: [TestStubs.Authenticators().Recovery({isEnrolled: false})],
  343. });
  344. const mock = Client.addMockResponse({
  345. url: AUTH_ENDPOINT,
  346. body: {all: true},
  347. method: 'DELETE',
  348. status: 204,
  349. });
  350. const wrapper = mountWithTheme(
  351. <AccountSecurityWrapper>
  352. <AccountSecurity />
  353. </AccountSecurityWrapper>,
  354. TestStubs.routerContext()
  355. );
  356. wrapper.find('Button[data-test-id="signoutAll"]').simulate('click');
  357. await tick();
  358. expect(window.location.assign).toHaveBeenCalledWith('/auth/login/');
  359. expect(mock).toHaveBeenCalled();
  360. });
  361. });