import {
render,
renderGlobalModal,
screen,
userEvent,
} from 'sentry-test/reactTestingLibrary';
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 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 () {
beforeEach(function () {
organization = TestStubs.Organization({teams});
routerContext = TestStubs.routerContext([{organization}]);
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('changes role to owner', function () {
render(, {
context: routerContext,
});
// Should have 4 roles
const radios = screen.getAllByRole('radio');
expect(radios).toHaveLength(4);
// Click last radio
userEvent.click(radios.at(-1));
expect(radios.at(-1)).toBeChecked();
// Save Member
userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
expect(updateMember).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
data: expect.objectContaining({
role: 'owner',
}),
})
);
});
it('leaves a team', function () {
render(, {
context: routerContext,
});
// Remove our one team
userEvent.click(screen.getByRole('button', {name: 'Remove'}));
// Save Member
userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
expect(updateMember).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
data: expect.objectContaining({
teams: [],
}),
})
);
});
it('joins a team', function () {
render(, {
context: routerContext,
});
// Should have one team enabled
expect(screen.getByTestId('team-row')).toBeInTheDocument();
// Select new team to join
// Open the dropdown
userEvent.click(screen.getByText('Add Team'));
// Click the first item
userEvent.click(screen.getByText('#new-team'));
// Save Member
userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
expect(updateMember).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
data: expect.objectContaining({
teams: ['team-slug', 'new-team'],
}),
})
);
});
});
describe('Cannot Edit', function () {
beforeEach(function () {
organization = TestStubs.Organization({teams, access: ['org:read']});
routerContext = TestStubs.routerContext([{organization}]);
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', function () {
render(, {
context: routerContext,
});
// Should have 4 roles
const radios = screen.getAllByRole('radio');
expect(radios.at(0)).toHaveAttribute('readonly');
// Save Member
expect(screen.getByRole('button', {name: 'Save Member'})).toBeDisabled();
});
});
describe('Display status', function () {
beforeEach(function () {
organization = TestStubs.Organization({teams, access: ['org:read']});
routerContext = TestStubs.routerContext([{organization}]);
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', function () {
render(, {
context: routerContext,
});
expect(screen.getByTestId('member-status')).toHaveTextContent('Invitation Pending');
});
it('display expired status', function () {
render(, {
context: routerContext,
});
expect(screen.getByTestId('member-status')).toHaveTextContent('Invitation Expired');
});
});
describe('Show resend button', function () {
beforeEach(function () {
organization = TestStubs.Organization({teams, access: ['org:read']});
routerContext = TestStubs.routerContext([{organization}]);
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', function () {
render(, {
context: routerContext,
});
expect(screen.getByRole('button', {name: 'Resend Invite'})).toBeInTheDocument();
});
it('does not show for expired', function () {
render(, {
context: routerContext,
});
expect(
screen.queryByRole('button', {name: 'Resend Invite'})
).not.toBeInTheDocument();
});
});
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,
}),
});
beforeEach(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 = () =>
screen.queryByRole('button', {name: 'Reset two-factor authentication'});
const tooltip = () => screen.queryByTestId('reset-2fa-tooltip');
const expectButtonEnabled = () => {
expect(button()).toHaveTextContent('Reset two-factor authentication');
expect(button()).toBeEnabled();
expect(tooltip()).not.toBeInTheDocument();
};
const expectButtonDisabled = async title => {
expect(button()).toHaveTextContent('Reset two-factor authentication');
expect(button()).toBeDisabled();
userEvent.hover(button());
expect(await screen.findByText(title)).toBeInTheDocument();
};
it('does not show for pending member', function () {
render(, {
context: routerContext,
});
expect(button()).not.toBeInTheDocument();
});
it('shows tooltip for joined member without permission to edit', async function () {
render(, {
context: routerContext,
});
await expectButtonDisabled('You do not have permission to perform this action');
});
it('shows tooltip for member without 2fa', async function () {
render(, {
context: routerContext,
});
await expectButtonDisabled('Not enrolled in two-factor authentication');
});
it('can reset member 2FA', function () {
const deleteMocks = has2fa.user.authenticators.map(auth =>
MockApiClient.addMockResponse({
url: `/users/${has2fa.user.id}/authenticators/${auth.id}/`,
method: 'DELETE',
})
);
render(, {
context: routerContext,
});
renderGlobalModal();
expectButtonEnabled();
userEvent.click(button());
userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
deleteMocks.forEach(deleteMock => {
expect(deleteMock).toHaveBeenCalled();
});
});
it('shows tooltip for member in multiple orgs', async function () {
render(, {
context: routerContext,
});
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;
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/members/${has2fa.id}/`,
body: has2fa,
});
render(, {
context: routerContext,
});
await expectButtonDisabled(
'Cannot be reset since two-factor is required for this organization'
);
});
});
});