123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- 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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {
- router: filterRouter,
- });
- await userEvent.type(await screen.findByPlaceholderText('Search Members'), 'member');
- filterRouter.location.query = {query: 'member'};
- rerender(<OrganizationMembersList />);
- 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(<OrganizationMembersList />, {
- 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(<OrganizationMembersList />);
- 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(<OrganizationMembersList />);
- 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(<OrganizationMembersList />);
- 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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {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(<OrganizationMembersList />, {organization});
- renderGlobalModal();
- expect(await screen.findByText('Members')).toBeInTheDocument();
- expect(screen.getByText(member.name)).toBeInTheDocument();
- });
- });
- });
|