import {AuthProviderFixture} from 'sentry-fixture/authProvider'; import {MemberFixture} from 'sentry-fixture/member'; import {MembersFixture} from 'sentry-fixture/members'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {RouterFixture} from 'sentry-fixture/routerFixture'; import {TeamFixture} from 'sentry-fixture/team'; import {UserFixture} from 'sentry-fixture/user'; import { render, renderGlobalModal, screen, userEvent, waitFor, within, } from 'sentry-test/reactTestingLibrary'; import selectEvent from 'sentry-test/selectEvent'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import ConfigStore from 'sentry/stores/configStore'; import ModalStore from 'sentry/stores/modalStore'; import OrganizationsStore from 'sentry/stores/organizationsStore'; import {trackAnalytics} from 'sentry/utils/analytics'; import {browserHistory} from 'sentry/utils/browserHistory'; import OrganizationMembersList from 'sentry/views/settings/organizationMembers/organizationMembersList'; jest.mock('sentry/utils/analytics'); jest.mock('sentry/actionCreators/indicator'); const roles = [ { id: 'admin', name: 'Admin', desc: 'This is the admin role', isAllowed: true, }, { id: 'member', name: 'Member', desc: 'This is the member role', isAllowed: true, }, { id: 'owner', name: 'Owner', desc: 'This is the owner role', isAllowed: true, }, ]; describe('OrganizationMembersList', function () { const members = MembersFixture(); const team = TeamFixture({slug: 'team'}); const member = MemberFixture({ id: '5', email: 'member@sentry.io', teams: [team.slug], teamRoles: [ { teamSlug: team.slug, role: null, }, ], flags: { 'sso:linked': true, 'idp:provisioned': false, 'idp:role-restricted': false, 'member-limit:restricted': false, 'partnership:restricted': false, 'sso:invalid': false, }, }); const currentUser = members[1]; currentUser.user = UserFixture({ ...currentUser, flags: {newsletter_consent_prompt: true}, }); const organization = OrganizationFixture({ access: ['member:admin', 'org:admin', 'member:write'], status: { id: 'active', name: 'active', }, }); const router = RouterFixture(); beforeEach(function () { ConfigStore.set('user', currentUser.user!); MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: '/organizations/org-slug/members/me/', method: 'GET', body: {roles}, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/members/', method: 'GET', body: [...MembersFixture(), member], }); MockApiClient.addMockResponse({ url: `/organizations/org-slug/members/${member.id}/`, body: member, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/access-requests/', method: 'GET', body: [ { id: 'pending-id', member: { id: 'pending-member-id', email: '', name: '', role: '', roleName: '', user: { id: '', name: 'sentry@test.com', }, }, team: TeamFixture(), }, ], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/auth-provider/', method: 'GET', body: { ...AuthProviderFixture(), require_link: true, }, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/teams/', method: 'GET', body: [TeamFixture(), team], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/invite-requests/', method: 'GET', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/missing-members/', method: 'GET', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/prompts-activity/', method: 'GET', body: { dismissed_ts: undefined, snoozed_ts: undefined, }, }); OrganizationsStore.load([organization]); ModalStore.init(); }); it('can remove a member', async function () { const deleteMock = MockApiClient.addMockResponse({ url: `/organizations/org-slug/members/${members[0].id}/`, method: 'DELETE', }); render(, {organization}); renderGlobalModal(); // The organization member row expect(await screen.findByTestId(members[0].email)).toBeInTheDocument(); await userEvent.click( within(screen.getByTestId(members[0].email)).getByRole('button', {name: 'Remove'}) ); await userEvent.click(await screen.findByRole('button', {name: 'Confirm'})); await waitFor(() => expect(addSuccessMessage).toHaveBeenCalled()); expect(deleteMock).toHaveBeenCalled(); expect(router.push).not.toHaveBeenCalled(); expect(OrganizationsStore.getAll()).toEqual([organization]); }); it('displays error message when failing to remove member', async function () { const deleteMock = MockApiClient.addMockResponse({ url: `/organizations/org-slug/members/${members[0].id}/`, method: 'DELETE', statusCode: 500, }); render(, {organization, router}); renderGlobalModal(); // The organization member row expect(await screen.findByTestId(members[0].email)).toBeInTheDocument(); await userEvent.click( within(screen.getByTestId(members[0].email)).getByRole('button', {name: 'Remove'}) ); await userEvent.click(await screen.findByRole('button', {name: 'Confirm'})); await waitFor(() => expect(addErrorMessage).toHaveBeenCalled()); expect(deleteMock).toHaveBeenCalled(); expect(router.push).not.toHaveBeenCalled(); expect(OrganizationsStore.getAll()).toEqual([organization]); }); it('can leave org', async function () { const deleteMock = MockApiClient.addMockResponse({ url: `/organizations/org-slug/members/${members[1].id}/`, method: 'DELETE', }); render(, {organization, router}); renderGlobalModal(); await userEvent.click(await screen.findByRole('button', {name: 'Leave'})); await userEvent.click(await screen.findByRole('button', {name: 'Confirm'})); await waitFor(() => expect(addSuccessMessage).toHaveBeenCalled()); expect(deleteMock).toHaveBeenCalled(); expect(browserHistory.push).toHaveBeenCalledTimes(1); expect(browserHistory.push).toHaveBeenCalledWith('/organizations/new/'); }); it('can redirect to remaining org after leaving', async function () { const deleteMock = MockApiClient.addMockResponse({ url: `/organizations/org-slug/members/${members[1].id}/`, method: 'DELETE', }); const secondOrg = OrganizationFixture({ slug: 'org-two', status: { id: 'active', name: 'active', }, }); OrganizationsStore.addOrReplace(secondOrg); render(, {organization, router}); renderGlobalModal(); await userEvent.click(await screen.findByRole('button', {name: 'Leave'})); await userEvent.click(screen.getByTestId('confirm-button')); await waitFor(() => expect(addSuccessMessage).toHaveBeenCalled()); expect(deleteMock).toHaveBeenCalled(); expect(browserHistory.push).toHaveBeenCalledTimes(1); expect(browserHistory.push).toHaveBeenCalledWith('/organizations/org-two/issues/'); expect(OrganizationsStore.getAll()).toEqual([secondOrg]); }); it('displays error message when failing to leave org', async function () { const deleteMock = MockApiClient.addMockResponse({ url: `/organizations/org-slug/members/${members[1].id}/`, method: 'DELETE', statusCode: 500, }); render(, {organization}); renderGlobalModal(); await userEvent.click(await screen.findByRole('button', {name: 'Leave'})); await userEvent.click(await screen.findByRole('button', {name: 'Confirm'})); await waitFor(() => expect(addErrorMessage).toHaveBeenCalled()); expect(deleteMock).toHaveBeenCalled(); expect(router.push).not.toHaveBeenCalled(); expect(OrganizationsStore.getAll()).toEqual([organization]); }); it('can re-send SSO link to member', async function () { const inviteMock = MockApiClient.addMockResponse({ url: `/organizations/org-slug/members/${members[0].id}/`, method: 'PUT', body: { id: '1234', }, }); render(, {organization}); expect(inviteMock).not.toHaveBeenCalled(); await userEvent.click(await screen.findByRole('button', {name: 'Resend SSO link'})); expect(inviteMock).toHaveBeenCalled(); }); it('can re-send invite to member', async function () { const inviteMock = MockApiClient.addMockResponse({ url: `/organizations/org-slug/members/${members[1].id}/`, method: 'PUT', body: { id: '1234', }, }); render(, {organization}); expect(inviteMock).not.toHaveBeenCalled(); await userEvent.click(await screen.findByRole('button', {name: 'Resend invite'})); expect(inviteMock).toHaveBeenCalled(); }); it('can search organization members', async function () { const filterRouter = RouterFixture(); const searchMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/members/', body: [], }); const {rerender} = render(, { router: filterRouter, }); await userEvent.type(await screen.findByPlaceholderText('Search Members'), 'member'); filterRouter.location.query = {query: 'member'}; rerender(); expect(searchMock).toHaveBeenLastCalledWith( '/organizations/org-slug/members/', expect.objectContaining({ method: 'GET', query: { query: 'member', }, }) ); await userEvent.keyboard('{enter}'); await waitFor(() => { expect(filterRouter.push).toHaveBeenCalledTimes(1); }); }); it('can filter members', async function () { const searchMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/members/', body: [], }); const filterRouter = RouterFixture(); const {rerender} = render(, { router: filterRouter, }); await userEvent.click(await screen.findByRole('button', {name: 'Filter'})); await userEvent.click(screen.getByRole('option', {name: 'Member'})); filterRouter.location.query = {query: 'role:member'}; rerender(); expect(searchMock).toHaveBeenLastCalledWith( '/organizations/org-slug/members/', expect.objectContaining({ method: 'GET', query: {query: 'role:member'}, }) ); await userEvent.click(screen.getByRole('option', {name: 'Member'})); for (const [filter, label] of [ ['isInvited', 'Invited'], ['has2fa', '2FA'], ['ssoLinked', 'SSO Linked'], ]) { const filterSection = screen.getByRole('listbox', {name: label}); await userEvent.click( within(filterSection).getByRole('option', { name: 'True', }) ); filterRouter.location.query = {query: `${filter}:true`}; rerender(); expect(searchMock).toHaveBeenLastCalledWith( '/organizations/org-slug/members/', expect.objectContaining({ method: 'GET', query: {query: `${filter}:true`}, }) ); await userEvent.click( within(filterSection).getByRole('option', { name: 'False', }) ); filterRouter.location.query = {query: `${filter}:false`}; rerender(); expect(searchMock).toHaveBeenLastCalledWith( '/organizations/org-slug/members/', expect.objectContaining({ method: 'GET', query: {query: `${filter}:false`}, }) ); await userEvent.click( within(filterSection).getByRole('option', { name: 'All', }) ); } }); describe('OrganizationInviteRequests', function () { const inviteRequest = MemberFixture({ id: '123', user: null, inviteStatus: 'requested_to_be_invited', inviterName: UserFixture().name, role: 'member', teams: [], }); const joinRequest = MemberFixture({ id: '456', user: null, email: 'test@gmail.com', inviteStatus: 'requested_to_join', role: 'member', teams: [], }); it('disable buttons for no access', async function () { const org = OrganizationFixture({ status: { id: 'active', name: 'active', }, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/invite-requests/', method: 'GET', body: [inviteRequest], }); MockApiClient.addMockResponse({ url: `/organizations/org-slug/invite-requests/${inviteRequest.id}/`, method: 'PUT', }); render(, {organization: org}); expect(await screen.findByText('Pending Members')).toBeInTheDocument(); expect(screen.getByRole('button', {name: 'Approve'})).toBeDisabled(); }); it('can approve invite request and update', async function () { const org = OrganizationFixture({ access: ['member:admin', 'org:admin', 'member:write'], status: { id: 'active', name: 'active', }, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/invite-requests/', method: 'GET', body: [inviteRequest], }); MockApiClient.addMockResponse({ url: `/organizations/org-slug/invite-requests/${inviteRequest.id}/`, method: 'PUT', }); render(, {organization}); expect(await screen.findByText('Pending Members')).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', {name: 'Approve'})); renderGlobalModal(); await userEvent.click(screen.getByTestId('confirm-button')); expect(screen.queryByText('Pending Members')).not.toBeInTheDocument(); expect(trackAnalytics).toHaveBeenCalledWith('invite_request.approved', { invite_status: inviteRequest.inviteStatus, member_id: parseInt(inviteRequest.id, 10), organization: org, }); }); it('can deny invite request and remove', async function () { const org = OrganizationFixture({ access: ['member:admin', 'org:admin', 'member:write'], status: { id: 'active', name: 'active', }, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/invite-requests/', method: 'GET', body: [joinRequest], }); MockApiClient.addMockResponse({ url: `/organizations/org-slug/invite-requests/${joinRequest.id}/`, method: 'DELETE', }); render(, {organization}); expect(await screen.findByText('Pending Members')).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', {name: 'Deny'})); expect(screen.queryByText('Pending Members')).not.toBeInTheDocument(); expect(trackAnalytics).toHaveBeenCalledWith('invite_request.denied', { invite_status: joinRequest.inviteStatus, member_id: parseInt(joinRequest.id, 10), organization: org, }); }); it('can update invite requests', async function () { const org = OrganizationFixture({ access: ['member:admin', 'org:admin', 'member:write'], status: { id: 'active', name: 'active', }, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/invite-requests/', method: 'GET', body: [inviteRequest], }); const updateWithApprove = MockApiClient.addMockResponse({ url: `/organizations/org-slug/invite-requests/${inviteRequest.id}/`, method: 'PUT', }); render(, {organization: org}); expect(await screen.findByText('Pending Members')).toBeInTheDocument(); await selectEvent.select(screen.getByRole('textbox', {name: 'Role: Member'}), [ 'Admin', ]); await userEvent.click(screen.getByRole('button', {name: 'Approve'})); renderGlobalModal(); await userEvent.click(screen.getByTestId('confirm-button')); expect(updateWithApprove).toHaveBeenCalledWith( `/organizations/org-slug/invite-requests/${inviteRequest.id}/`, expect.objectContaining({data: expect.objectContaining({role: 'admin'})}) ); }); }); describe('Org Access Requests', function () { it('can invite member', async function () { const inviteOrg = OrganizationFixture({ features: ['invite-members'], access: ['member:admin', 'org:admin', 'member:write'], status: { id: 'active', name: 'active', }, }); render(, {organization: inviteOrg}); renderGlobalModal(); await userEvent.click(await screen.findByRole('button', {name: 'Invite Members'})); expect(screen.getByRole('dialog')).toBeInTheDocument(); }); it('can not invite members without the invite-members feature', async function () { const org = OrganizationFixture({ features: [], access: ['member:admin', 'org:admin', 'member:write'], status: { id: 'active', name: 'active', }, }); render(, {organization: org}); renderGlobalModal(); expect(await screen.findByRole('button', {name: 'Invite Members'})).toBeDisabled(); }); it('cannot invite members if SSO is required', async function () { const org = OrganizationFixture({ features: ['invite-members'], access: [], status: { id: 'active', name: 'active', }, requiresSso: true, }); render(, {organization: org}); renderGlobalModal(); await userEvent.click(screen.getByRole('button', {name: 'Invite Members'})); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); it('can invite without permissions', async function () { const org = OrganizationFixture({ features: ['invite-members'], access: [], status: { id: 'active', name: 'active', }, }); render(, {organization: org}); renderGlobalModal(); await userEvent.click(await screen.findByRole('button', {name: 'Invite Members'})); expect(screen.getByRole('dialog')).toBeInTheDocument(); }); it('renders member list', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/members/', method: 'GET', body: [member], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/prompts-activity/', method: 'GET', body: {}, }); render(, {organization}); renderGlobalModal(); expect(await screen.findByText('Members')).toBeInTheDocument(); expect(screen.getByText(member.name)).toBeInTheDocument(); }); }); });