123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798 |
- import {UserEnrolledAuthenticatorFixture} from 'sentry-fixture/authenticators';
- import {MemberFixture} from 'sentry-fixture/member';
- import {OrganizationFixture} from 'sentry-fixture/organization';
- import {OrgRoleListFixture} from 'sentry-fixture/roleList';
- import {TeamFixture} from 'sentry-fixture/team';
- import {UserFixture} from 'sentry-fixture/user';
- import {initializeOrg} from 'sentry-test/initializeOrg';
- import {
- cleanup,
- render,
- renderGlobalModal,
- screen,
- userEvent,
- within,
- } from 'sentry-test/reactTestingLibrary';
- import selectEvent from 'sentry-test/selectEvent';
- import {updateMember} from 'sentry/actionCreators/members';
- import TeamStore from 'sentry/stores/teamStore';
- import type {Member} from 'sentry/types/organization';
- import OrganizationMemberDetail from './organizationMemberDetail';
- jest.mock('sentry/actionCreators/members', () => ({
- updateMember: jest.fn().mockReturnValue(new Promise(() => {})),
- }));
- describe('OrganizationMemberDetail', function () {
- const team = TeamFixture();
- const idpTeam = TeamFixture({
- id: '3',
- slug: 'idp-member-team',
- name: 'Idp Member Team',
- isMember: true,
- flags: {
- 'idp:provisioned': true,
- },
- });
- const teams = [
- team,
- TeamFixture({
- id: '2',
- slug: 'new-team',
- name: 'New Team',
- isMember: false,
- }),
- idpTeam,
- ];
- const teamAssignment = {
- teams: [team.slug],
- teamRoles: [
- {
- teamSlug: team.slug,
- role: null,
- },
- ],
- };
- const member = MemberFixture({
- roles: OrgRoleListFixture(),
- dateCreated: new Date().toISOString(),
- ...teamAssignment,
- });
- const pendingMember = MemberFixture({
- id: '2',
- roles: OrgRoleListFixture(),
- dateCreated: new Date().toISOString(),
- ...teamAssignment,
- invite_link: 'http://example.com/i/abc123',
- pending: true,
- });
- const expiredMember = MemberFixture({
- id: '3',
- roles: OrgRoleListFixture(),
- dateCreated: new Date().toISOString(),
- ...teamAssignment,
- invite_link: 'http://example.com/i/abc123',
- pending: true,
- expired: true,
- });
- const idpTeamMember = MemberFixture({
- id: '4',
- roles: OrgRoleListFixture(),
- dateCreated: new Date().toISOString(),
- teams: [idpTeam.slug],
- teamRoles: [
- {
- teamSlug: idpTeam.slug,
- role: null,
- },
- ],
- });
- const managerMember = MemberFixture({
- id: '6',
- roles: OrgRoleListFixture(),
- role: 'manager',
- });
- beforeEach(() => {
- MockApiClient.clearMockResponses();
- TeamStore.loadInitialData(teams);
- });
- describe('Can Edit', function () {
- const organization = OrganizationFixture({features: ['team-roles']});
- beforeEach(function () {
- TeamStore.init();
- TeamStore.loadInitialData(teams);
- jest.resetAllMocks();
- 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}/members/${idpTeamMember.id}/`,
- body: idpTeamMember,
- });
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/members/${managerMember.id}/`,
- body: managerMember,
- });
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/teams/`,
- body: teams,
- });
- });
- it('changes org role to owner', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: member.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- // Should have 5 roles
- const radios = await screen.findAllByRole('radio');
- expect(radios).toHaveLength(5);
- // Click last radio
- await userEvent.click(radios.at(-1) as Element);
- expect(radios.at(-1)).toBeChecked();
- // Save Member
- await userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
- expect(updateMember).toHaveBeenCalledWith(
- expect.anything(),
- expect.objectContaining({
- data: expect.objectContaining({
- orgRole: 'owner',
- }),
- })
- );
- });
- it('leaves a team', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: member.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- // Remove our one team
- await userEvent.click(await screen.findByRole('button', {name: 'Remove'}));
- // Save Member
- await userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
- expect(updateMember).toHaveBeenCalledWith(
- expect.anything(),
- expect.objectContaining({
- data: expect.objectContaining({
- teamRoles: [],
- }),
- })
- );
- });
- it('cannot leave idp-provisioned team', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: idpTeamMember.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- expect(await screen.findByRole('button', {name: 'Remove'})).toBeDisabled();
- });
- it('joins a team and assign a team-role', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: member.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- // Should have one team enabled
- expect(await screen.findByTestId('team-row-for-member')).toBeInTheDocument();
- // Select new team to join
- // Open the dropdown
- await userEvent.click(screen.getByText('Add Team'));
- // Click the first item
- await userEvent.click(screen.getByText('#new-team'));
- // Assign as admin to new team
- const teamRoleSelect = screen.getAllByText('Contributor')[0];
- await selectEvent.select(teamRoleSelect, ['Team Admin']);
- // Save Member
- await userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
- expect(updateMember).toHaveBeenCalledWith(
- expect.anything(),
- expect.objectContaining({
- data: expect.objectContaining({
- teamRoles: [
- {teamSlug: 'team-slug', role: null},
- {teamSlug: 'new-team', role: 'admin'},
- ],
- }),
- })
- );
- });
- it('cannot join idp-provisioned team', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: member.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- await userEvent.click(await screen.findByText('Add Team'));
- await userEvent.hover(screen.getByText('#idp-member-team'));
- expect(
- await screen.findByText(
- "Membership to this team is managed through your organization's identity provider."
- )
- ).toBeInTheDocument();
- });
- });
- describe('Cannot Edit', function () {
- const organization = OrganizationFixture({access: ['org:read']});
- beforeEach(function () {
- TeamStore.init();
- TeamStore.loadInitialData(teams);
- jest.resetAllMocks();
- 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('can not change roles, teams, or save', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: member.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- // Should have 4 roles
- const radios = await screen.findAllByRole('radio');
- expect(radios.at(0)).toHaveAttribute('readonly');
- // Save Member
- expect(screen.getByRole('button', {name: 'Save Member'})).toBeDisabled();
- });
- });
- describe('Display status', function () {
- const organization = OrganizationFixture({access: ['org:read']});
- beforeEach(function () {
- TeamStore.init();
- TeamStore.loadInitialData(teams);
- jest.resetAllMocks();
- 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('display pending status', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: pendingMember.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- expect(await screen.findByTestId('member-status')).toHaveTextContent(
- 'Invitation Pending'
- );
- });
- it('display expired status', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: expiredMember.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- expect(await screen.findByTestId('member-status')).toHaveTextContent(
- 'Invitation Expired'
- );
- });
- });
- describe('Show resend button', function () {
- const organization = OrganizationFixture({access: ['org:read']});
- beforeEach(function () {
- TeamStore.init();
- TeamStore.loadInitialData(teams);
- jest.resetAllMocks();
- 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('shows for pending', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: pendingMember.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- expect(
- await screen.findByRole('button', {name: 'Resend Invite'})
- ).toBeInTheDocument();
- });
- it('does not show for expired', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: expiredMember.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- await screen.findAllByRole('radio');
- expect(
- screen.queryByRole('button', {name: 'Resend Invite'})
- ).not.toBeInTheDocument();
- });
- });
- describe('Reset member 2FA', function () {
- const fields = {
- roles: OrgRoleListFixture(),
- dateCreated: new Date().toISOString(),
- ...teamAssignment,
- };
- const noAccess = MemberFixture({
- ...fields,
- id: '4',
- user: UserFixture({has2fa: false, authenticators: undefined}),
- });
- const no2fa = MemberFixture({
- ...fields,
- id: '5',
- user: UserFixture({has2fa: false, authenticators: [], canReset2fa: true}),
- });
- const has2fa = MemberFixture({
- ...fields,
- id: '6',
- user: UserFixture({
- has2fa: true,
- authenticators: [
- UserEnrolledAuthenticatorFixture({type: 'totp', id: 'totp'}),
- UserEnrolledAuthenticatorFixture({type: 'sms', id: 'sms'}),
- UserEnrolledAuthenticatorFixture({type: 'u2f', id: 'u2f'}),
- ],
- canReset2fa: true,
- }),
- });
- const multipleOrgs = MemberFixture({
- ...fields,
- id: '7',
- user: UserFixture({
- has2fa: true,
- authenticators: [UserEnrolledAuthenticatorFixture({type: 'totp', id: 'totp'})],
- canReset2fa: false,
- }),
- });
- const organization = OrganizationFixture();
- beforeEach(function () {
- 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 tooltip = () => screen.queryByTestId('reset-2fa-tooltip');
- const expectButtonEnabled = async () => {
- const button = await screen.findByRole('button', {
- name: 'Reset two-factor authentication',
- });
- expect(button).toHaveTextContent('Reset two-factor authentication');
- expect(button).toBeEnabled();
- expect(tooltip()).not.toBeInTheDocument();
- };
- const expectButtonDisabled = async (title: string) => {
- const button = await screen.findByRole('button', {
- name: 'Reset two-factor authentication',
- });
- expect(button).toHaveTextContent('Reset two-factor authentication');
- expect(button).toBeDisabled();
- await userEvent.hover(button);
- expect(await screen.findByText(title)).toBeInTheDocument();
- };
- it('does not show for pending member', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: pendingMember.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- expect(
- await screen.findByRole('button', {name: 'Resend Invite'})
- ).toBeInTheDocument();
- expect(
- screen.queryByRole('button', {name: 'Reset two-factor authentication'})
- ).not.toBeInTheDocument();
- });
- it('shows tooltip for joined member without permission to edit', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: noAccess.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- await expectButtonDisabled('You do not have permission to perform this action');
- });
- it('shows tooltip for member without 2fa', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: no2fa.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- await expectButtonDisabled('Not enrolled in two-factor authentication');
- });
- it('can reset member 2FA', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: has2fa.id}},
- });
- const deleteMocks = (has2fa.user?.authenticators || []).map(auth =>
- MockApiClient.addMockResponse({
- url: `/users/${has2fa.user?.id}/authenticators/${auth.id}/`,
- method: 'DELETE',
- })
- );
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- renderGlobalModal();
- await expectButtonEnabled();
- await userEvent.click(
- screen.getByRole('button', {name: 'Reset two-factor authentication'})
- );
- await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
- deleteMocks.forEach(deleteMock => {
- expect(deleteMock).toHaveBeenCalled();
- });
- });
- it('shows tooltip for member in multiple orgs', async function () {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: multipleOrgs.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- await expectButtonDisabled(
- 'Cannot be reset since user is in more than one organization'
- );
- });
- it('shows tooltip for member in 2FA required org', async function () {
- organization.require2FA = true;
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: has2fa.id}},
- });
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/members/${has2fa.id}/`,
- body: has2fa,
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- await expectButtonDisabled(
- 'Cannot be reset since two-factor is required for this organization'
- );
- });
- });
- describe('Org Roles affect Team Roles', () => {
- // Org Admin will be deprecated
- const admin = MemberFixture({
- id: '4',
- role: 'admin',
- roleName: 'Admin',
- orgRole: 'admin',
- ...teamAssignment,
- });
- const manager = MemberFixture({
- id: '5',
- role: 'manager',
- roleName: 'Manager',
- orgRole: 'manager',
- ...teamAssignment,
- });
- const owner = MemberFixture({
- id: '6',
- role: 'owner',
- roleName: 'Owner',
- orgRole: 'owner',
- ...teamAssignment,
- });
- const organization = OrganizationFixture({features: ['team-roles']});
- beforeEach(() => {
- MockApiClient.clearMockResponses();
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/members/${member.id}/`,
- body: member,
- });
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/members/${admin.id}/`,
- body: admin,
- });
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/members/${manager.id}/`,
- body: manager,
- });
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/members/${owner.id}/`,
- body: owner,
- });
- });
- it('does not overwrite team-roles for org members', async () => {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: member.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- // Role info box is hidden
- expect(screen.queryByTestId('alert-role-overwrite')).not.toBeInTheDocument();
- // Dropdown has correct value set
- const teamRow = await screen.findByTestId('team-row-for-member');
- const teamRoleSelect = within(teamRow).getByText('Contributor');
- // Dropdown options are not visible
- expect(screen.queryAllByText('...').length).toBe(0);
- // Dropdown can be opened
- await selectEvent.openMenu(teamRoleSelect);
- expect(screen.queryAllByText('...').length).toBe(2);
- // Dropdown value can be changed
- await userEvent.click(screen.getByLabelText('Team Admin'));
- expect(teamRoleSelect).toHaveTextContent('Team Admin');
- });
- it('overwrite team-roles for org admin/manager/owner', async () => {
- async function testForOrgRole(testMember: Member) {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: testMember.id}},
- });
- cleanup();
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- // Role info box is showed
- expect(await screen.findByTestId('alert-role-overwrite')).toBeInTheDocument();
- // Dropdown has correct value set
- const teamRow = screen.getByTestId('team-row-for-member');
- const teamRoleSelect = within(teamRow).getByText('Team Admin');
- // Dropdown options are not visible
- expect(screen.queryAllByText('...').length).toBe(0);
- // Dropdown cannot be opened
- await selectEvent.openMenu(teamRoleSelect);
- expect(screen.queryAllByText('...').length).toBe(0);
- }
- for (const role of [admin, manager, owner]) {
- await testForOrgRole(role);
- }
- });
- it('overwrites when changing from member to manager', async () => {
- const {router} = initializeOrg({
- organization,
- router: {params: {memberId: member.id}},
- });
- render(<OrganizationMemberDetail />, {
- router,
- organization,
- });
- // Dropdown has correct value set
- const teamRow = await screen.findByTestId('team-row-for-member');
- const teamRoleSelect = within(teamRow).getByText('Contributor');
- // Role info box is hidden
- expect(screen.queryByTestId('alert-role-overwrite')).not.toBeInTheDocument();
- // Change member to owner
- const orgRoleRadio = screen.getAllByRole('radio');
- expect(orgRoleRadio).toHaveLength(5);
- await userEvent.click(orgRoleRadio.at(-1) as Element);
- expect(orgRoleRadio.at(-1)).toBeChecked();
- // Role info box is shown
- expect(screen.queryByTestId('alert-role-overwrite')).toBeInTheDocument();
- // Dropdown has correct value set
- within(teamRow).getByText('Team Admin');
- // Dropdown options are not visible
- expect(screen.queryAllByText('...').length).toBe(0);
- // Dropdown cannot be opened
- await selectEvent.openMenu(teamRoleSelect);
- expect(screen.queryAllByText('...').length).toBe(0);
- });
- });
- });
|