import {mountWithTheme} from 'sentry-test/enzyme'; import {mountGlobalModal} from 'sentry-test/modal'; import {Client} from 'sentry/api'; import ModalStore from 'sentry/stores/modalStore'; import AccountSecurity from 'sentry/views/settings/account/accountSecurity'; import AccountSecurityWrapper from 'sentry/views/settings/account/accountSecurity/accountSecurityWrapper'; const ENDPOINT = '/users/me/authenticators/'; const ORG_ENDPOINT = '/organizations/'; const ACCOUNT_EMAILS_ENDPOINT = '/users/me/emails/'; const AUTH_ENDPOINT = '/auth/'; describe('AccountSecurity', function () { beforeEach(function () { jest.spyOn(window.location, 'assign').mockImplementation(() => {}); Client.clearMockResponses(); Client.addMockResponse({ url: ORG_ENDPOINT, body: TestStubs.Organizations(), }); Client.addMockResponse({ url: ACCOUNT_EMAILS_ENDPOINT, body: TestStubs.AccountEmails(), }); }); afterEach(function () { window.location.assign.mockRestore(); }); it('renders empty', function () { Client.addMockResponse({ url: ENDPOINT, body: [], }); const wrapper = mountWithTheme( , TestStubs.routerContext() ); expect(wrapper.find('EmptyMessage')).toHaveLength(1); expect(wrapper.find('TwoFactorRequired')).toHaveLength(0); }); it('renders a primary interface that is enrolled', function () { Client.addMockResponse({ url: ENDPOINT, body: [TestStubs.Authenticators().Totp({configureButton: 'Info'})], }); const wrapper = mountWithTheme( , TestStubs.routerContext() ); expect(wrapper.find('AuthenticatorName').prop('children')).toBe('Authenticator App'); // There should be an "Info" button expect( wrapper.find('Button[className="details-button"]').first().prop('children') ).toBe('Info'); // Remove button expect(wrapper.find('button[aria-label="delete"]')).toHaveLength(1); expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true); expect(wrapper.find('TwoFactorRequired')).toHaveLength(0); }); it('can delete enrolled authenticator', async function () { Client.addMockResponse({ url: ENDPOINT, body: [ TestStubs.Authenticators().Totp({ authId: '15', configureButton: 'Info', }), ], }); const deleteMock = Client.addMockResponse({ url: `${ENDPOINT}15/`, method: 'DELETE', }); expect(deleteMock).not.toHaveBeenCalled(); const wrapper = mountWithTheme( , TestStubs.routerContext() ); expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true); // next authenticators request should have totp disabled const authenticatorsMock = Client.addMockResponse({ url: ENDPOINT, body: [ TestStubs.Authenticators().Totp({ isEnrolled: false, authId: '15', configureButton: 'Info', }), ], }); // This will open confirm modal wrapper.find('button[aria-label="delete"]').simulate('click'); // Confirm const modal = await mountGlobalModal(); modal.find('Button').last().simulate('click'); await tick(); wrapper.update(); expect(deleteMock).toHaveBeenCalled(); // Should only have been called once expect(authenticatorsMock).toHaveBeenCalledTimes(1); expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(false); // No enrolled authenticators expect(wrapper.find('TwoFactorRequired')).toHaveLength(1); }); it('can remove one of multiple 2fa methods when org requires 2fa', async function () { Client.addMockResponse({ url: ENDPOINT, body: [ TestStubs.Authenticators().Totp({ authId: '15', configureButton: 'Info', }), TestStubs.Authenticators().U2f(), ], }); Client.addMockResponse({ url: ORG_ENDPOINT, body: TestStubs.Organizations({require2FA: true}), }); const deleteMock = Client.addMockResponse({ url: `${ENDPOINT}15/`, method: 'DELETE', }); expect(deleteMock).not.toHaveBeenCalled(); const wrapper = mountWithTheme( , TestStubs.routerContext() ); expect(wrapper.find('AuthenticatorStatus').first().prop('enabled')).toBe(true); expect(wrapper.find('RemoveConfirm').first().prop('disabled')).toBe(false); expect(wrapper.find('Tooltip').first().prop('disabled')).toBe(true); // This will open confirm modal wrapper.find('button[aria-label="delete"]').first().simulate('click'); // Confirm const modal = await mountGlobalModal(); modal.find('Button').last().simulate('click'); expect(deleteMock).toHaveBeenCalled(); }); it('can not remove last 2fa method when org requires 2fa', function () { Client.addMockResponse({ url: ENDPOINT, body: [ TestStubs.Authenticators().Totp({ authId: '15', configureButton: 'Info', }), ], }); Client.addMockResponse({ url: ORG_ENDPOINT, body: TestStubs.Organizations({require2FA: true}), }); const deleteMock = Client.addMockResponse({ url: `${ENDPOINT}15/`, method: 'DELETE', }); expect(deleteMock).not.toHaveBeenCalled(); const wrapper = mountWithTheme( , TestStubs.routerContext() ); expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true); expect(wrapper.find('RemoveConfirm').prop('disabled')).toBe(true); expect(wrapper.find('Tooltip').prop('disabled')).toBe(false); expect(wrapper.find('Tooltip').prop('title')).toContain('test 1 and test 2'); // This will open confirm modal wrapper.find('button[aria-label="delete"]').simulate('click'); // Confirm expect(wrapper.find('Modal Button')).toHaveLength(0); expect(deleteMock).not.toHaveBeenCalled(); }); it('cannot enroll without verified email', async function () { Client.addMockResponse({ url: ENDPOINT, body: [TestStubs.Authenticators().Totp({isEnrolled: false})], }); Client.addMockResponse({ url: ACCOUNT_EMAILS_ENDPOINT, body: [ { email: 'primary@example.com', isPrimary: true, isVerified: false, }, ], }); const wrapper = mountWithTheme( , TestStubs.routerContext() ); expect(wrapper.find('AuthenticatorName').prop('children')).toBe('Authenticator App'); // There should be an "Add" button expect( wrapper.find('Button[className="enroll-button"]').first().prop('children') ).toBe('Add'); expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(false); // user is not 2fa enrolled expect(wrapper.find('TwoFactorRequired')).toHaveLength(1); // expect modal to be called const openEmailModalFunc = jest.spyOn(ModalStore, 'openModal'); const Add2FAButton = wrapper.find('Button[className="enroll-button"]').first(); Add2FAButton.simulate('click'); await tick(); expect(openEmailModalFunc).toHaveBeenCalled(); }); it('renders a backup interface that is not enrolled', function () { Client.addMockResponse({ url: ENDPOINT, body: [TestStubs.Authenticators().Recovery({isEnrolled: false})], }); const wrapper = mountWithTheme( , TestStubs.routerContext() ); expect(wrapper.find('AuthenticatorName').prop('children')).toBe('Recovery Codes'); // There should be an View Codes button expect(wrapper.find('Button[className="details-button"]')).toHaveLength(0); expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(false); // user is not 2fa enrolled expect(wrapper.find('TwoFactorRequired')).toHaveLength(1); }); it('renders a primary interface that is not enrolled', function () { Client.addMockResponse({ url: ENDPOINT, body: [TestStubs.Authenticators().Totp({isEnrolled: false})], }); const wrapper = mountWithTheme( , TestStubs.routerContext() ); expect(wrapper.find('AuthenticatorName').prop('children')).toBe('Authenticator App'); // There should be an "Add" button expect( wrapper.find('Button[className="enroll-button"]').first().prop('children') ).toBe('Add'); expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(false); // user is not 2fa enrolled expect(wrapper.find('TwoFactorRequired')).toHaveLength(1); }); it('does not render primary interface that disallows new enrollments', function () { Client.addMockResponse({ url: ENDPOINT, body: [ TestStubs.Authenticators().Totp({disallowNewEnrollment: false}), TestStubs.Authenticators().U2f({disallowNewEnrollment: null}), TestStubs.Authenticators().Sms({disallowNewEnrollment: true}), ], }); const wrapper = mountWithTheme( , TestStubs.routerContext() ); // There should only be two authenticators rendered expect(wrapper.find('AuthenticatorName')).toHaveLength(2); }); it('renders primary interface if new enrollments are disallowed, but we are enrolled', function () { Client.addMockResponse({ url: ENDPOINT, body: [ TestStubs.Authenticators().Sms({isEnrolled: true, disallowNewEnrollment: true}), ], }); const wrapper = mountWithTheme( , TestStubs.routerContext() ); // Should still render the authenticator since we are already enrolled expect(wrapper.find('AuthenticatorName')).toHaveLength(1); expect(wrapper.find('AuthenticatorName').prop('children')).toBe('Text Message'); }); it('renders a backup interface that is enrolled', function () { Client.addMockResponse({ url: ENDPOINT, body: [TestStubs.Authenticators().Recovery({isEnrolled: true})], }); const wrapper = mountWithTheme( , TestStubs.routerContext() ); expect(wrapper.find('AuthenticatorName').prop('children')).toBe('Recovery Codes'); // There should be an View Codes button expect( wrapper.find('Button[className="details-button"]').first().prop('children') ).toBe('View Codes'); expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true); }); it('can change password', function () { Client.addMockResponse({ url: ENDPOINT, body: [TestStubs.Authenticators().Recovery({isEnrolled: false})], }); const url = '/users/me/password/'; const mock = Client.addMockResponse({ url, method: 'PUT', }); const wrapper = mountWithTheme( , TestStubs.routerContext() ); wrapper .find('PasswordForm input[name="password"]') .simulate('change', {target: {value: 'oldpassword'}}); wrapper .find('PasswordForm input[name="passwordNew"]') .simulate('change', {target: {value: 'newpassword'}}); wrapper .find('PasswordForm input[name="passwordVerify"]') .simulate('change', {target: {value: 'newpassword'}}); wrapper.find('PasswordForm form').simulate('submit'); expect(mock).toHaveBeenCalledWith( url, expect.objectContaining({ method: 'PUT', data: { password: 'oldpassword', passwordNew: 'newpassword', passwordVerify: 'newpassword', }, }) ); // user is not 2fa enrolled expect(wrapper.find('TwoFactorRequired')).toHaveLength(1); }); it('requires current password to be entered', function () { Client.addMockResponse({ url: ENDPOINT, body: [TestStubs.Authenticators().Recovery({isEnrolled: false})], }); const url = '/users/me/password/'; const mock = Client.addMockResponse({ url, method: 'PUT', }); const wrapper = mountWithTheme( , TestStubs.routerContext() ); wrapper .find('PasswordForm input[name="passwordNew"]') .simulate('change', {target: {value: 'newpassword'}}); wrapper .find('PasswordForm input[name="passwordVerify"]') .simulate('change', {target: {value: 'newpassword'}}); wrapper.find('PasswordForm form').simulate('submit'); expect(mock).not.toHaveBeenCalled(); // user is not 2fa enrolled expect(wrapper.find('TwoFactorRequired')).toHaveLength(1); }); it('can expire all sessions', async function () { Client.addMockResponse({ url: ENDPOINT, body: [TestStubs.Authenticators().Recovery({isEnrolled: false})], }); const mock = Client.addMockResponse({ url: AUTH_ENDPOINT, body: {all: true}, method: 'DELETE', status: 204, }); const wrapper = mountWithTheme( , TestStubs.routerContext() ); wrapper.find('Button[data-test-id="signoutAll"]').simulate('click'); await tick(); expect(window.location.assign).toHaveBeenCalledWith('/auth/login/'); expect(mock).toHaveBeenCalled(); }); });