import {mountWithTheme} from 'sentry-test/enzyme'; import {mountGlobalModal} from 'sentry-test/modal'; import {updateMember} from 'sentry/actionCreators/members'; import TeamStore from 'sentry/stores/teamStore'; import OrganizationMemberDetail from 'sentry/views/settings/organizationMembers/organizationMemberDetail'; jest.mock('sentry/actionCreators/members', () => ({ updateMember: jest.fn().mockReturnValue(new Promise(() => {})), })); describe('OrganizationMemberDetail', function () { let organization; let wrapper; let routerContext; const team = TestStubs.Team(); const teams = [ team, TestStubs.Team({ id: '2', slug: 'new-team', name: 'New Team', isMember: false, }), ]; const member = TestStubs.Member({ roles: TestStubs.OrgRoleList(), dateCreated: new Date(), teams: [team.slug], }); const pendingMember = TestStubs.Member({ id: 2, roles: TestStubs.OrgRoleList(), dateCreated: new Date(), teams: [team.slug], invite_link: 'http://example.com/i/abc123', pending: true, }); const expiredMember = TestStubs.Member({ id: 3, roles: TestStubs.OrgRoleList(), dateCreated: new Date(), teams: [team.slug], invite_link: 'http://example.com/i/abc123', pending: true, expired: true, }); describe('Can Edit', function () { beforeAll(function () { organization = TestStubs.Organization({teams}); routerContext = TestStubs.routerContext([{organization}]); }); TeamStore.loadInitialData(teams); beforeEach(function () { MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/members/${member.id}/`, body: member, }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/members/${pendingMember.id}/`, body: pendingMember, }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/members/${expiredMember.id}/`, body: expiredMember, }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/teams/`, body: teams, }); }); it('changes role to owner', function () { wrapper = mountWithTheme( , routerContext ); // Should have 4 roles expect(wrapper.find('OrganizationRoleSelect Radio')).toHaveLength(4); wrapper.find('OrganizationRoleSelect Radio').last().simulate('click'); expect(wrapper.find('OrganizationRoleSelect Radio').last().prop('checked')).toBe( true ); // Save Member wrapper.find('Button[priority="primary"]').simulate('click'); expect(updateMember).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ data: expect.objectContaining({ role: 'owner', }), }) ); }); it('leaves a team', async function () { wrapper = mountWithTheme( , routerContext ); // Wait for team list to load await tick(); // Remove our one team const button = wrapper.find('TeamSelect TeamRow Button'); expect(button).toHaveLength(1); button.simulate('click'); // Save Member wrapper.find('Button[priority="primary"]').simulate('click'); expect(updateMember).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ data: expect.objectContaining({ teams: [], }), }) ); }); it('joins a team', function () { wrapper = mountWithTheme( , routerContext ); // Wait for team list to fetch. wrapper.update(); // Should have one team enabled expect(wrapper.find('TeamPanelItem')).toHaveLength(1); // Select new team to join // Open the dropdown wrapper.find('TeamSelect DropdownButton').simulate('click'); // Click the first item wrapper.find('TeamSelect [title="new team"]').at(0).simulate('click'); // Save Member wrapper.find('Button[priority="primary"]').simulate('click'); expect(updateMember).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ data: expect.objectContaining({ teams: ['team-slug', 'new-team'], }), }) ); }); }); describe('Cannot Edit', function () { beforeAll(function () { organization = TestStubs.Organization({teams, access: ['org:read']}); routerContext = TestStubs.routerContext([{organization}]); }); it('can not change roles, teams, or save', function () { wrapper = mountWithTheme( , routerContext ); wrapper.update(); // Should have 4 roles expect(wrapper.find('OrganizationRoleSelect').prop('disabled')).toBe(true); expect(wrapper.find('TeamSelect').prop('disabled')).toBe(true); expect(wrapper.find('TeamRow Button').first().prop('disabled')).toBe(true); // Save Member expect(wrapper.find('Button[priority="primary"]').prop('disabled')).toBe(true); }); }); describe('Display status', function () { beforeAll(function () { organization = TestStubs.Organization({teams, access: ['org:read']}); routerContext = TestStubs.routerContext([{organization}]); }); it('display pending status', function () { wrapper = mountWithTheme( , routerContext ); expect(wrapper.find('[data-test-id="member-status"]').text()).toEqual( 'Invitation Pending' ); }); it('display expired status', function () { wrapper = mountWithTheme( , routerContext ); expect(wrapper.find('[data-test-id="member-status"]').text()).toEqual( 'Invitation Expired' ); }); }); describe('Show resend button', function () { beforeAll(function () { organization = TestStubs.Organization({teams, access: ['org:read']}); routerContext = TestStubs.routerContext([{organization}]); }); it('shows for pending', function () { wrapper = mountWithTheme( , routerContext ); const button = wrapper.find('Button[data-test-id="resend-invite"]'); expect(button.text()).toEqual('Resend Invite'); }); it('does not show for expired', function () { wrapper = mountWithTheme( , routerContext ); expect(wrapper.find('Button[data-test-id="resend-invite"]')).toHaveLength(0); }); }); describe('Reset member 2FA', function () { const fields = { roles: TestStubs.OrgRoleList(), dateCreated: new Date(), teams: [team.slug], }; const noAccess = TestStubs.Member({ ...fields, id: '4', user: TestStubs.User({has2fa: false}), }); const no2fa = TestStubs.Member({ ...fields, id: '5', user: TestStubs.User({has2fa: false, authenticators: [], canReset2fa: true}), }); const has2fa = TestStubs.Member({ ...fields, id: '6', user: TestStubs.User({ has2fa: true, authenticators: [ TestStubs.Authenticators().Totp(), TestStubs.Authenticators().Sms(), TestStubs.Authenticators().U2f(), ], canReset2fa: true, }), }); const multipleOrgs = TestStubs.Member({ ...fields, id: '7', user: TestStubs.User({ has2fa: true, authenticators: [TestStubs.Authenticators().Totp()], canReset2fa: false, }), }); beforeAll(function () { organization = TestStubs.Organization({teams}); routerContext = TestStubs.routerContext([{organization}]); MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/members/${pendingMember.id}/`, body: pendingMember, }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/members/${noAccess.id}/`, body: noAccess, }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/members/${no2fa.id}/`, body: no2fa, }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/members/${has2fa.id}/`, body: has2fa, }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/members/${multipleOrgs.id}/`, body: multipleOrgs, }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/teams/`, body: teams, }); }); const button = 'Button[data-test-id="reset-2fa"]'; const tooltip = 'Tooltip[data-test-id="reset-2fa-tooltip"]'; const expectButtonEnabled = () => { expect(wrapper.find(button).text()).toEqual('Reset two-factor authentication'); expect(wrapper.find(button).prop('disabled')).toBe(false); expect(wrapper.find(tooltip).prop('title')).toEqual(''); expect(wrapper.find(tooltip).prop('disabled')).toBe(true); }; const expectButtonDisabled = title => { expect(wrapper.find(button).text()).toEqual('Reset two-factor authentication'); expect(wrapper.find(button).prop('disabled')).toBe(true); expect(wrapper.find(tooltip).prop('title')).toEqual(title); expect(wrapper.find(tooltip).prop('disabled')).toBe(false); }; it('does not show for pending member', function () { wrapper = mountWithTheme( , routerContext ); expect(wrapper.find(button)).toHaveLength(0); }); it('shows tooltip for joined member without permission to edit', function () { wrapper = mountWithTheme( , routerContext ); expectButtonDisabled('You do not have permission to perform this action'); }); it('shows tooltip for member without 2fa', function () { wrapper = mountWithTheme( , routerContext ); expectButtonDisabled('Not enrolled in two-factor authentication'); }); it('can reset member 2FA', async function () { const deleteMocks = has2fa.user.authenticators.map(auth => MockApiClient.addMockResponse({ url: `/users/${has2fa.user.id}/authenticators/${auth.id}/`, method: 'DELETE', }) ); wrapper = mountWithTheme( , routerContext ); expectButtonEnabled(); wrapper.find(button).simulate('click'); const modal = await mountGlobalModal(); modal.find('Button[data-test-id="confirm-button"]').simulate('click'); deleteMocks.forEach(deleteMock => { expect(deleteMock).toHaveBeenCalled(); }); }); it('shows tooltip for member in multiple orgs', function () { wrapper = mountWithTheme( , routerContext ); expectButtonDisabled('Cannot be reset since user is in more than one organization'); }); it('shows tooltip for member in 2FA required org', function () { organization.require2FA = true; MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/members/${has2fa.id}/`, body: has2fa, }); wrapper = mountWithTheme( , routerContext ); expectButtonDisabled( 'Cannot be reset since two-factor is required for this organization' ); }); }); });