Commit 22cd7dc4 authored by Peter Hegman's avatar Peter Hegman

Merge branch '350999-invite-modal-using-invitations-api' into 'master'

Allow invitations API to handle user invites as well as emails

See merge request gitlab-org/gitlab!80733
parents cc977bb1 3f7c9ba7
...@@ -156,13 +156,7 @@ const Api = { ...@@ -156,13 +156,7 @@ const Api = {
}); });
}, },
addGroupMembersByUserId(id, data) { inviteGroupMembers(id, data) {
const url = Api.buildUrl(this.groupMembersPath).replace(':id', encodeURIComponent(id));
return axios.post(url, data);
},
inviteGroupMembersByEmail(id, data) {
const url = Api.buildUrl(this.groupInvitationsPath).replace(':id', encodeURIComponent(id)); const url = Api.buildUrl(this.groupInvitationsPath).replace(':id', encodeURIComponent(id));
return axios.post(url, data); return axios.post(url, data);
...@@ -258,13 +252,7 @@ const Api = { ...@@ -258,13 +252,7 @@ const Api = {
.then(({ data }) => data); .then(({ data }) => data);
}, },
addProjectMembersByUserId(id, data) { inviteProjectMembers(id, data) {
const url = Api.buildUrl(this.projectMembersPath).replace(':id', encodeURIComponent(id));
return axios.post(url, data);
},
inviteProjectMembersByEmail(id, data) {
const url = Api.buildUrl(this.projectInvitationsPath).replace(':id', encodeURIComponent(id)); const url = Api.buildUrl(this.projectInvitationsPath).replace(':id', encodeURIComponent(id));
return axios.post(url, data); return axios.post(url, data);
......
...@@ -193,46 +193,28 @@ export default { ...@@ -193,46 +193,28 @@ export default {
this.invalidFeedbackMessage = ''; this.invalidFeedbackMessage = '';
const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite(); const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite();
const promises = [];
const baseData = { const apiAddByInvite = this.isProject
? Api.inviteProjectMembers.bind(Api)
: Api.inviteGroupMembers.bind(Api);
const email = usersToInviteByEmail !== '' ? { email: usersToInviteByEmail } : {};
const userId = usersToAddById !== '' ? { user_id: usersToAddById } : {};
this.trackinviteMembersForTask();
apiAddByInvite(this.id, {
format: 'json', format: 'json',
expires_at: expiresAt, expires_at: expiresAt,
access_level: accessLevel, access_level: accessLevel,
invite_source: this.source, invite_source: this.source,
tasks_to_be_done: this.tasksToBeDoneForPost, tasks_to_be_done: this.tasksToBeDoneForPost,
tasks_project_id: this.tasksProjectForPost, tasks_project_id: this.tasksProjectForPost,
}; ...email,
...userId,
if (usersToInviteByEmail !== '') { })
const apiInviteByEmail = this.isProject .then((response) => {
? Api.inviteProjectMembersByEmail.bind(Api) const message = responseMessageFromSuccess(response);
: Api.inviteGroupMembersByEmail.bind(Api);
promises.push(
apiInviteByEmail(this.id, {
...baseData,
email: usersToInviteByEmail,
}),
);
}
if (usersToAddById !== '') {
const apiAddByUserId = this.isProject
? Api.addProjectMembersByUserId.bind(Api)
: Api.addGroupMembersByUserId.bind(Api);
promises.push(
apiAddByUserId(this.id, {
...baseData,
user_id: usersToAddById,
}),
);
}
this.trackinviteMembersForTask();
Promise.all(promises)
.then((responses) => {
const message = responseMessageFromSuccess(responses);
if (message) { if (message) {
this.showInvalidFeedbackMessage({ this.showInvalidFeedbackMessage({
......
import { __, s__ } from '~/locale'; import { s__ } from '~/locale';
export const SEARCH_DELAY = 200; export const SEARCH_DELAY = 200;
...@@ -14,9 +14,6 @@ export const GROUP_FILTERS = { ...@@ -14,9 +14,6 @@ export const GROUP_FILTERS = {
DESCENDANT_GROUPS: 'descendant_groups', DESCENDANT_GROUPS: 'descendant_groups',
}; };
export const API_MESSAGES = {
EMAIL_ALREADY_INVITED: __('Invite email has already been taken'),
};
export const USERS_FILTER_ALL = 'all'; export const USERS_FILTER_ALL = 'all';
export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id'; export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id';
export const TRIGGER_ELEMENT_BUTTON = 'button'; export const TRIGGER_ELEMENT_BUTTON = 'button';
......
import { isString } from 'lodash'; import { isString } from 'lodash';
import { API_MESSAGES } from '~/invite_members/constants';
function responseKeyedMessageParsed(keyedMessage) { function responseKeyedMessageParsed(keyedMessage) {
try { try {
const keys = Object.keys(keyedMessage); const keys = Object.keys(keyedMessage);
const msg = keyedMessage[keys[0]]; const msg = keyedMessage[keys[0]];
if (msg === API_MESSAGES.EMAIL_ALREADY_INVITED) {
return '';
}
return msg; return msg;
} catch { } catch {
return ''; return '';
} }
} }
function responseMessageStringForMultiple(message) {
return message.includes(':');
}
function responseMessageStringFirstPart(message) {
const firstPart = message.split(':')[1];
const firstMsg = firstPart.split(/ and [\w-]*$/)[0].trim();
return firstMsg;
}
export function responseMessageFromError(response) { export function responseMessageFromError(response) {
if (!response?.response?.data) { if (!response?.response?.data) {
...@@ -33,36 +20,25 @@ export function responseMessageFromError(response) { ...@@ -33,36 +20,25 @@ export function responseMessageFromError(response) {
response: { data }, response: { data },
} = response; } = response;
return ( return data.error || data.message?.error || data.message || '';
data.error ||
data.message?.user?.[0] ||
data.message?.access_level?.[0] ||
data.message?.error ||
data.message ||
''
);
} }
export function responseMessageFromSuccess(response) { export function responseMessageFromSuccess(response) {
if (!response?.[0]?.data) { if (!response?.data) {
return ''; return '';
} }
const { data } = response[0]; const { data } = response;
if (data.message && !data.message.user) { if (data.message) {
const { message } = data; const { message } = data;
if (isString(message)) { if (isString(message)) {
if (responseMessageStringForMultiple(message)) {
return responseMessageStringFirstPart(message);
}
return message; return message;
} }
return responseKeyedMessageParsed(message); return responseKeyedMessageParsed(message);
} }
return data.message || data.message?.user || data.error || ''; return data.error || '';
} }
...@@ -20605,9 +20605,6 @@ msgstr "" ...@@ -20605,9 +20605,6 @@ msgstr ""
msgid "Invite a group" msgid "Invite a group"
msgstr "" msgstr ""
msgid "Invite email has already been taken"
msgstr ""
msgid "Invite members" msgid "Invite members"
msgstr "" msgstr ""
......
...@@ -42,27 +42,6 @@ RSpec.describe 'Groups > Members > Manage members' do ...@@ -42,27 +42,6 @@ RSpec.describe 'Groups > Members > Manage members' do
end end
end end
it 'add user to group', :js, :snowplow, :aggregate_failures do
group.add_owner(user1)
visit group_group_members_path(group)
invite_member(user2.name, role: 'Reporter')
page.within(second_row) do
expect(page).to have_content(user2.name)
expect(page).to have_button('Reporter')
end
expect_snowplow_event(
category: 'Members::CreateService',
action: 'create_member',
label: 'group-members-page',
property: 'existing_user',
user: user1
)
end
it 'remove user from group', :js do it 'remove user from group', :js do
group.add_owner(user1) group.add_owner(user1)
group.add_developer(user2) group.add_developer(user2)
...@@ -87,43 +66,29 @@ RSpec.describe 'Groups > Members > Manage members' do ...@@ -87,43 +66,29 @@ RSpec.describe 'Groups > Members > Manage members' do
end end
end end
it 'add yourself to group when already an owner', :js, :aggregate_failures do context 'when inviting' do
it 'add yourself to group when already an owner', :js do
group.add_owner(user1) group.add_owner(user1)
visit group_group_members_path(group) visit group_group_members_path(group)
invite_member(user1.name, role: 'Reporter') invite_member(user1.name, role: 'Reporter', refresh: false)
page.within(first_row) do expect(page).to have_selector(invite_modal_selector)
expect(page).to have_content(user1.name) expect(page).to have_content("not authorized to update member")
page.refresh
page.within find_member_row(user1) do
expect(page).to have_content('Owner') expect(page).to have_content('Owner')
end end
end end
it 'invite user to group', :js, :snowplow do it_behaves_like 'inviting members', 'group-members-page' do
group.add_owner(user1) let_it_be(:entity) { group }
let_it_be(:members_page_path) { group_group_members_path(entity) }
visit group_group_members_path(group) let_it_be(:subentity) { create(:group, parent: group) }
let_it_be(:subentity_members_page_path) { group_group_members_path(subentity) }
invite_member('test@example.com', role: 'Reporter')
expect(page).to have_link 'Invited'
click_link 'Invited'
aggregate_failures do
page.within(members_table) do
expect(page).to have_content('test@example.com')
expect(page).to have_content('Invited')
expect(page).to have_button('Reporter')
end
expect_snowplow_event(
category: 'Members::InviteService',
action: 'create_member',
label: 'group-members-page',
property: 'net_new_user',
user: user1
)
end end
end end
......
...@@ -48,24 +48,6 @@ RSpec.describe 'Projects > Members > Manage members', :js do ...@@ -48,24 +48,6 @@ RSpec.describe 'Projects > Members > Manage members', :js do
end end
end end
it 'add user to project', :snowplow, :aggregate_failures do
visit_members_page
invite_member(user2.name, role: 'Reporter')
page.within find_member_row(user2) do
expect(page).to have_button('Reporter')
end
expect_snowplow_event(
category: 'Members::CreateService',
action: 'create_member',
label: 'project-members-page',
property: 'existing_user',
user: user1
)
end
it 'uses ProjectMember access_level_roles for the invite members modal access option', :aggregate_failures do it 'uses ProjectMember access_level_roles for the invite members modal access option', :aggregate_failures do
visit_members_page visit_members_page
...@@ -104,24 +86,11 @@ RSpec.describe 'Projects > Members > Manage members', :js do ...@@ -104,24 +86,11 @@ RSpec.describe 'Projects > Members > Manage members', :js do
expect(members_table).not_to have_content(other_user.name) expect(members_table).not_to have_content(other_user.name)
end end
it 'invite user to project', :snowplow, :aggregate_failures do it_behaves_like 'inviting members', 'project-members-page' do
visit_members_page let_it_be(:entity) { project }
let_it_be(:members_page_path) { project_project_members_path(entity) }
invite_member('test@example.com', role: 'Reporter') let_it_be(:subentity) { project }
let_it_be(:subentity_members_page_path) { project_project_members_path(entity) }
click_link 'Invited'
page.within find_invited_member_row('test@example.com') do
expect(page).to have_button('Reporter')
end
expect_snowplow_event(
category: 'Members::InviteService',
action: 'create_member',
label: 'project-members-page',
property: 'net_new_user',
user: user1
)
end end
describe 'member search results' do describe 'member search results' do
......
...@@ -187,36 +187,15 @@ describe('Api', () => { ...@@ -187,36 +187,15 @@ describe('Api', () => {
}); });
}); });
describe('addGroupMembersByUserId', () => { describe('inviteGroupMembers', () => {
it('adds an existing User as a new Group Member by User ID', () => {
const groupId = 1;
const expectedUserId = 2;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/1/members`;
const params = {
user_id: expectedUserId,
access_level: 10,
expires_at: undefined,
};
mock.onPost(expectedUrl).reply(200, {
id: expectedUserId,
state: 'active',
});
return Api.addGroupMembersByUserId(groupId, params).then(({ data }) => {
expect(data.id).toBe(expectedUserId);
expect(data.state).toBe('active');
});
});
});
describe('inviteGroupMembersByEmail', () => {
it('invites a new email address to create a new User and become a Group Member', () => { it('invites a new email address to create a new User and become a Group Member', () => {
const groupId = 1; const groupId = 1;
const email = 'email@example.com'; const email = 'email@example.com';
const userId = '1';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/1/invitations`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/1/invitations`;
const params = { const params = {
email, email,
userId,
access_level: 10, access_level: 10,
expires_at: undefined, expires_at: undefined,
}; };
...@@ -225,7 +204,7 @@ describe('Api', () => { ...@@ -225,7 +204,7 @@ describe('Api', () => {
status: 'success', status: 'success',
}); });
return Api.inviteGroupMembersByEmail(groupId, params).then(({ data }) => { return Api.inviteGroupMembers(groupId, params).then(({ data }) => {
expect(data.status).toBe('success'); expect(data.status).toBe('success');
}); });
}); });
...@@ -543,36 +522,15 @@ describe('Api', () => { ...@@ -543,36 +522,15 @@ describe('Api', () => {
}); });
}); });
describe('addProjectMembersByUserId', () => { describe('inviteProjectMembers', () => {
it('adds an existing User as a new Project Member by User ID', () => {
const projectId = 1;
const expectedUserId = 2;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/members`;
const params = {
user_id: expectedUserId,
access_level: 10,
expires_at: undefined,
};
mock.onPost(expectedUrl).reply(200, {
id: expectedUserId,
state: 'active',
});
return Api.addProjectMembersByUserId(projectId, params).then(({ data }) => {
expect(data.id).toBe(expectedUserId);
expect(data.state).toBe('active');
});
});
});
describe('inviteProjectMembersByEmail', () => {
it('invites a new email address to create a new User and become a Project Member', () => { it('invites a new email address to create a new User and become a Project Member', () => {
const projectId = 1; const projectId = 1;
const expectedEmail = 'email@example.com'; const email = 'email@example.com';
const userId = '1';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/invitations`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/invitations`;
const params = { const params = {
email: expectedEmail, email,
userId,
access_level: 10, access_level: 10,
expires_at: undefined, expires_at: undefined,
}; };
...@@ -581,7 +539,7 @@ describe('Api', () => { ...@@ -581,7 +539,7 @@ describe('Api', () => {
status: 'success', status: 'success',
}); });
return Api.inviteProjectMembersByEmail(projectId, params).then(({ data }) => { return Api.inviteProjectMembers(projectId, params).then(({ data }) => {
expect(data.status).toBe('success'); expect(data.status).toBe('success');
}); });
}); });
......
const INVITATIONS_API_EMAIL_INVALID = { const EMAIL_INVALID = {
message: { error: 'email contains an invalid email address' }, message: { error: 'email contains an invalid email address' },
}; };
const INVITATIONS_API_ERROR_EMAIL_INVALID = { const ERROR_EMAIL_INVALID = {
error: 'email contains an invalid email address', error: 'email contains an invalid email address',
}; };
const INVITATIONS_API_EMAIL_RESTRICTED = { const EMAIL_RESTRICTED = {
message: { message: {
'email@example.com': 'email@example.com':
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.", "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
...@@ -14,65 +14,31 @@ const INVITATIONS_API_EMAIL_RESTRICTED = { ...@@ -14,65 +14,31 @@ const INVITATIONS_API_EMAIL_RESTRICTED = {
status: 'error', status: 'error',
}; };
const INVITATIONS_API_MULTIPLE_EMAIL_RESTRICTED = { const MULTIPLE_RESTRICTED = {
message: { message: {
'email@example.com': 'email@example.com':
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.", "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
'email4@example.com': 'email4@example.com':
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.", "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.",
}, root:
status: 'error',
};
const INVITATIONS_API_EMAIL_TAKEN = {
message: {
'email@example.org': 'Invite email has already been taken',
},
status: 'error',
};
const MEMBERS_API_MEMBER_ALREADY_EXISTS = {
message: 'Member already exists',
};
const MEMBERS_API_SINGLE_USER_RESTRICTED = {
message: {
user: [
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.", "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
],
}, },
status: 'error',
}; };
const MEMBERS_API_SINGLE_USER_ACCESS_LEVEL = { const EMAIL_TAKEN = {
message: { message: {
access_level: [ 'email@example.org': "The member's email address has already been taken",
'should be greater than or equal to Owner inherited membership from group Gitlab Org',
],
}, },
};
const MEMBERS_API_MULTIPLE_USERS_RESTRICTED = {
message:
"root: The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups. and user18: The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist. and john_doe31: The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Email restrictions for sign-ups.",
status: 'error', status: 'error',
}; };
export const apiPaths = { export const GROUPS_INVITATIONS_PATH = '/api/v4/groups/1/invitations';
GROUPS_MEMBERS: '/api/v4/groups/1/members',
GROUPS_INVITATIONS: '/api/v4/groups/1/invitations',
};
export const membersApiResponse = {
MEMBER_ALREADY_EXISTS: MEMBERS_API_MEMBER_ALREADY_EXISTS,
SINGLE_USER_ACCESS_LEVEL: MEMBERS_API_SINGLE_USER_ACCESS_LEVEL,
SINGLE_USER_RESTRICTED: MEMBERS_API_SINGLE_USER_RESTRICTED,
MULTIPLE_USERS_RESTRICTED: MEMBERS_API_MULTIPLE_USERS_RESTRICTED,
};
export const invitationsApiResponse = { export const invitationsApiResponse = {
EMAIL_INVALID: INVITATIONS_API_EMAIL_INVALID, EMAIL_INVALID,
ERROR_EMAIL_INVALID: INVITATIONS_API_ERROR_EMAIL_INVALID, ERROR_EMAIL_INVALID,
EMAIL_RESTRICTED: INVITATIONS_API_EMAIL_RESTRICTED, EMAIL_RESTRICTED,
MULTIPLE_EMAIL_RESTRICTED: INVITATIONS_API_MULTIPLE_EMAIL_RESTRICTED, MULTIPLE_RESTRICTED,
EMAIL_TAKEN: INVITATIONS_API_EMAIL_TAKEN, EMAIL_TAKEN,
}; };
...@@ -2,23 +2,19 @@ import { ...@@ -2,23 +2,19 @@ import {
responseMessageFromSuccess, responseMessageFromSuccess,
responseMessageFromError, responseMessageFromError,
} from '~/invite_members/utils/response_message_parser'; } from '~/invite_members/utils/response_message_parser';
import { membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses'; import { invitationsApiResponse } from '../mock_data/api_responses';
describe('Response message parser', () => { describe('Response message parser', () => {
const expectedMessage = 'expected display and message.'; const expectedMessage = 'expected display and message.';
describe('parse message from successful response', () => { describe('parse message from successful response', () => {
const exampleKeyedMsg = { 'email@example.com': expectedMessage }; const exampleKeyedMsg = { 'email@example.com': expectedMessage };
const exampleFirstPartMultiple = 'username1: expected display and message.';
const exampleUserMsgMultiple =
' and username2: id not found and restricted email. and username3: email is restricted.';
it.each([ it.each([
[[{ data: { message: expectedMessage } }]], [{ data: { message: expectedMessage } }],
[[{ data: { message: exampleFirstPartMultiple + exampleUserMsgMultiple } }]], [{ data: { error: expectedMessage } }],
[[{ data: { error: expectedMessage } }]], [{ data: { message: [expectedMessage] } }],
[[{ data: { message: [expectedMessage] } }]], [{ data: { message: exampleKeyedMsg } }],
[[{ data: { message: exampleKeyedMsg } }]],
])(`returns "${expectedMessage}" from success response: %j`, (successResponse) => { ])(`returns "${expectedMessage}" from success response: %j`, (successResponse) => {
expect(responseMessageFromSuccess(successResponse)).toBe(expectedMessage); expect(responseMessageFromSuccess(successResponse)).toBe(expectedMessage);
}); });
...@@ -27,8 +23,6 @@ describe('Response message parser', () => { ...@@ -27,8 +23,6 @@ describe('Response message parser', () => {
describe('message from error response', () => { describe('message from error response', () => {
it.each([ it.each([
[{ response: { data: { error: expectedMessage } } }], [{ response: { data: { error: expectedMessage } } }],
[{ response: { data: { message: { user: [expectedMessage] } } } }],
[{ response: { data: { message: { access_level: [expectedMessage] } } } }],
[{ response: { data: { message: { error: expectedMessage } } } }], [{ response: { data: { message: { error: expectedMessage } } } }],
[{ response: { data: { message: expectedMessage } } }], [{ response: { data: { message: expectedMessage } } }],
])(`returns "${expectedMessage}" from error response: %j`, (errorResponse) => { ])(`returns "${expectedMessage}" from error response: %j`, (errorResponse) => {
...@@ -41,18 +35,10 @@ describe('Response message parser', () => { ...@@ -41,18 +35,10 @@ describe('Response message parser', () => {
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups."; "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.";
it.each([ it.each([
[[{ data: membersApiResponse.MULTIPLE_USERS_RESTRICTED }]], [{ data: invitationsApiResponse.MULTIPLE_RESTRICTED }],
[[{ data: invitationsApiResponse.MULTIPLE_EMAIL_RESTRICTED }]], [{ data: invitationsApiResponse.EMAIL_RESTRICTED }],
[[{ data: invitationsApiResponse.EMAIL_RESTRICTED }]],
])(`returns "${expectedMessage}" from success response: %j`, (restrictedResponse) => { ])(`returns "${expectedMessage}" from success response: %j`, (restrictedResponse) => {
expect(responseMessageFromSuccess(restrictedResponse)).toBe(expected); expect(responseMessageFromSuccess(restrictedResponse)).toBe(expected);
}); });
it.each([[{ response: { data: membersApiResponse.SINGLE_USER_RESTRICTED } }]])(
`returns "${expectedMessage}" from error response: %j`,
(singleRestrictedResponse) => {
expect(responseMessageFromError(singleRestrictedResponse)).toBe(expected);
},
);
}); });
}); });
...@@ -5,19 +5,22 @@ module Spec ...@@ -5,19 +5,22 @@ module Spec
module Helpers module Helpers
module Features module Features
module InviteMembersModalHelper module InviteMembersModalHelper
def invite_member(name, role: 'Guest', expires_at: nil) def invite_member(names, role: 'Guest', expires_at: nil, refresh: true)
click_on 'Invite members' click_on 'Invite members'
page.within invite_modal_selector do page.within invite_modal_selector do
Array.wrap(names).each do |name|
find(member_dropdown_selector).set(name) find(member_dropdown_selector).set(name)
wait_for_requests wait_for_requests
click_button name click_button name
end
choose_options(role, expires_at) choose_options(role, expires_at)
click_button 'Invite' click_button 'Invite'
page.refresh page.refresh if refresh
end end
end end
......
# frozen_string_literal: true
RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
before_all do
group.add_owner(user1)
end
it 'adds user as member', :js, :snowplow, :aggregate_failures do
visit members_page_path
invite_member(user2.name, role: 'Reporter')
page.within find_member_row(user2) do
expect(page).to have_button('Reporter')
end
expect_snowplow_event(
category: 'Members::InviteService',
action: 'create_member',
label: snowplow_invite_label,
property: 'existing_user',
user: user1
)
end
it 'invites user by email', :js, :snowplow, :aggregate_failures do
visit members_page_path
invite_member('test@example.com', role: 'Reporter')
click_link 'Invited'
page.within find_invited_member_row('test@example.com') do
expect(page).to have_button('Reporter')
end
expect_snowplow_event(
category: 'Members::InviteService',
action: 'create_member',
label: snowplow_invite_label,
property: 'net_new_user',
user: user1
)
end
it 'invites user by username and invites user by email', :js, :aggregate_failures do
visit members_page_path
invite_member([user2.name, 'test@example.com'], role: 'Reporter')
page.within find_member_row(user2) do
expect(page).to have_button('Reporter')
end
click_link 'Invited'
page.within find_invited_member_row('test@example.com') do
expect(page).to have_button('Reporter')
end
end
context 'when member is already a member by username' do
it 'updates the member for that user', :js do
visit members_page_path
invite_member(user2.name, role: 'Developer')
invite_member(user2.name, role: 'Reporter', refresh: false)
expect(page).not_to have_selector(invite_modal_selector)
page.refresh
page.within find_invited_member_row(user2.name) do
expect(page).to have_button('Reporter')
end
end
end
context 'when member is already a member by email' do
it 'fails with an error', :js do
visit members_page_path
invite_member('test@example.com', role: 'Developer')
invite_member('test@example.com', role: 'Reporter', refresh: false)
expect(page).to have_selector(invite_modal_selector)
expect(page).to have_content("The member's email address has already been taken")
page.refresh
click_link 'Invited'
page.within find_invited_member_row('test@example.com') do
expect(page).to have_button('Developer')
end
end
end
context 'when inviting a parent group member to the sub-entity' do
before_all do
group.add_owner(user1)
group.add_developer(user2)
end
context 'when role is higher than parent group membership' do
let(:role) { 'Maintainer' }
it 'adds the user as a member on sub-entity with higher access level', :js do
visit subentity_members_page_path
invite_member(user2.name, role: role, refresh: false)
expect(page).not_to have_selector(invite_modal_selector)
page.refresh
page.within find_invited_member_row(user2.name) do
expect(page).to have_button(role)
end
end
end
context 'when role is lower than parent group membership' do
let(:role) { 'Reporter' }
it 'fails with an error', :js do
visit subentity_members_page_path
invite_member(user2.name, role: role, refresh: false)
expect(page).to have_selector(invite_modal_selector)
expect(page).to have_content "Access level should be greater than or equal to Developer inherited membership " \
"from group #{group.name}"
page.refresh
page.within find_invited_member_row(user2.name) do
expect(page).to have_content('Developer')
expect(page).not_to have_button('Developer')
end
end
context 'when there are multiple users invited with errors' do
let_it_be(:user3) { create(:user) }
before do
group.add_maintainer(user3)
end
it 'only shows the first user error', :js do
visit subentity_members_page_path
invite_member([user2.name, user3.name], role: role, refresh: false)
expect(page).to have_selector(invite_modal_selector)
expect(page).to have_text("Access level should be greater than or equal to", count: 1)
page.refresh
page.within find_invited_member_row(user2.name) do
expect(page).to have_content('Developer')
expect(page).not_to have_button('Developer')
end
page.within find_invited_member_row(user3.name) do
expect(page).to have_content('Maintainer')
expect(page).not_to have_button('Maintainer')
end
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment