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
group.add_owner(user1) it 'add yourself to group when already an owner', :js do
group.add_owner(user1)
visit group_group_members_path(group)
invite_member(user1.name, role: 'Reporter')
page.within(first_row) do
expect(page).to have_content(user1.name)
expect(page).to have_content('Owner')
end
end
it 'invite user to group', :js, :snowplow do visit group_group_members_path(group)
group.add_owner(user1)
visit group_group_members_path(group) invite_member(user1.name, role: 'Reporter', refresh: false)
invite_member('test@example.com', role: 'Reporter') expect(page).to have_selector(invite_modal_selector)
expect(page).to have_content("not authorized to update member")
expect(page).to have_link 'Invited' page.refresh
click_link 'Invited'
aggregate_failures do page.within find_member_row(user1) do
page.within(members_table) do expect(page).to have_content('Owner')
expect(page).to have_content('test@example.com')
expect(page).to have_content('Invited')
expect(page).to have_button('Reporter')
end end
end
expect_snowplow_event( it_behaves_like 'inviting members', 'group-members-page' do
category: 'Members::InviteService', let_it_be(:entity) { group }
action: 'create_member', let_it_be(:members_page_path) { group_group_members_path(entity) }
label: 'group-members-page', let_it_be(:subentity) { create(:group, parent: group) }
property: 'net_new_user', let_it_be(:subentity_members_page_path) { group_group_members_path(subentity) }
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');
}); });
}); });
......
...@@ -23,7 +23,7 @@ import ContentTransition from '~/vue_shared/components/content_transition.vue'; ...@@ -23,7 +23,7 @@ import ContentTransition from '~/vue_shared/components/content_transition.vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import { getParameterValues } from '~/lib/utils/url_utility'; import { getParameterValues } from '~/lib/utils/url_utility';
import { apiPaths, membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses'; import { GROUPS_INVITATIONS_PATH, invitationsApiResponse } from '../mock_data/api_responses';
import { import {
propsData, propsData,
inviteSource, inviteSource,
...@@ -301,11 +301,8 @@ describe('InviteMembersModal', () => { ...@@ -301,11 +301,8 @@ describe('InviteMembersModal', () => {
}); });
describe('submitting the invite form', () => { describe('submitting the invite form', () => {
const mockMembersApi = (code, data) => {
mock.onPost(apiPaths.GROUPS_MEMBERS).reply(code, data);
};
const mockInvitationsApi = (code, data) => { const mockInvitationsApi = (code, data) => {
mock.onPost(apiPaths.GROUPS_INVITATIONS).reply(code, data); mock.onPost(GROUPS_INVITATIONS_PATH).reply(code, data);
}; };
const expectedEmailRestrictedError = const expectedEmailRestrictedError =
...@@ -329,7 +326,7 @@ describe('InviteMembersModal', () => { ...@@ -329,7 +326,7 @@ describe('InviteMembersModal', () => {
await triggerMembersTokenSelect([user1, user2]); await triggerMembersTokenSelect([user1, user2]);
wrapper.vm.$toast = { show: jest.fn() }; wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData }); jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
}); });
describe('when triggered from regular mounting', () => { describe('when triggered from regular mounting', () => {
...@@ -337,12 +334,8 @@ describe('InviteMembersModal', () => { ...@@ -337,12 +334,8 @@ describe('InviteMembersModal', () => {
clickInviteButton(); clickInviteButton();
}); });
it('sets isLoading on the Invite button when it is clicked', () => { it('calls Api inviteGroupMembers with the correct params', () => {
expect(findModal().props('actionPrimary').attributes.loading).toBe(true); expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
});
it('calls Api addGroupMembersByUserId with the correct params', () => {
expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(propsData.id, postData);
}); });
it('displays the successful toastMessage', () => { it('displays the successful toastMessage', () => {
...@@ -372,21 +365,9 @@ describe('InviteMembersModal', () => { ...@@ -372,21 +365,9 @@ describe('InviteMembersModal', () => {
await triggerMembersTokenSelect([user1]); await triggerMembersTokenSelect([user1]);
}); });
it('displays "Member already exists" api message for http status conflict', async () => {
mockMembersApi(httpStatus.CONFLICT, membersApiResponse.MEMBER_ALREADY_EXISTS);
clickInviteButton();
await waitForPromises();
expect(membersFormGroupInvalidFeedback()).toBe('Member already exists');
expect(findMembersSelect().props('validationState')).toBe(false);
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
});
describe('clearing the invalid state and message', () => { describe('clearing the invalid state and message', () => {
beforeEach(async () => { beforeEach(async () => {
mockMembersApi(httpStatus.CONFLICT, membersApiResponse.MEMBER_ALREADY_EXISTS); mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN);
clickInviteButton(); clickInviteButton();
...@@ -394,7 +375,9 @@ describe('InviteMembersModal', () => { ...@@ -394,7 +375,9 @@ describe('InviteMembersModal', () => {
}); });
it('clears the error when the list of members to invite is cleared', async () => { it('clears the error when the list of members to invite is cleared', async () => {
expect(membersFormGroupInvalidFeedback()).toBe('Member already exists'); expect(membersFormGroupInvalidFeedback()).toBe(
Object.values(invitationsApiResponse.EMAIL_TAKEN.message)[0],
);
expect(findMembersSelect().props('validationState')).toBe(false); expect(findMembersSelect().props('validationState')).toBe(false);
findMembersSelect().vm.$emit('clear'); findMembersSelect().vm.$emit('clear');
...@@ -425,13 +408,15 @@ describe('InviteMembersModal', () => { ...@@ -425,13 +408,15 @@ describe('InviteMembersModal', () => {
}); });
it('clears the invalid state and message once the list of members to invite is cleared', async () => { it('clears the invalid state and message once the list of members to invite is cleared', async () => {
mockMembersApi(httpStatus.CONFLICT, membersApiResponse.MEMBER_ALREADY_EXISTS); mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN);
clickInviteButton(); clickInviteButton();
await waitForPromises(); await waitForPromises();
expect(membersFormGroupInvalidFeedback()).toBe('Member already exists'); expect(membersFormGroupInvalidFeedback()).toBe(
Object.values(invitationsApiResponse.EMAIL_TAKEN.message)[0],
);
expect(findMembersSelect().props('validationState')).toBe(false); expect(findMembersSelect().props('validationState')).toBe(false);
expect(findModal().props('actionPrimary').attributes.loading).toBe(false); expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
...@@ -445,7 +430,10 @@ describe('InviteMembersModal', () => { ...@@ -445,7 +430,10 @@ describe('InviteMembersModal', () => {
}); });
it('displays the generic error for http server error', async () => { it('displays the generic error for http server error', async () => {
mockMembersApi(httpStatus.INTERNAL_SERVER_ERROR, 'Request failed with status code 500'); mockInvitationsApi(
httpStatus.INTERNAL_SERVER_ERROR,
'Request failed with status code 500',
);
clickInviteButton(); clickInviteButton();
...@@ -455,7 +443,7 @@ describe('InviteMembersModal', () => { ...@@ -455,7 +443,7 @@ describe('InviteMembersModal', () => {
}); });
it('displays the restricted user api message for response with bad request', async () => { it('displays the restricted user api message for response with bad request', async () => {
mockMembersApi(httpStatus.BAD_REQUEST, membersApiResponse.SINGLE_USER_RESTRICTED); mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
clickInviteButton(); clickInviteButton();
...@@ -465,7 +453,7 @@ describe('InviteMembersModal', () => { ...@@ -465,7 +453,7 @@ describe('InviteMembersModal', () => {
}); });
it('displays the first part of the error when multiple existing users are restricted by email', async () => { it('displays the first part of the error when multiple existing users are restricted by email', async () => {
mockMembersApi(httpStatus.CREATED, membersApiResponse.MULTIPLE_USERS_RESTRICTED); mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
clickInviteButton(); clickInviteButton();
...@@ -476,19 +464,6 @@ describe('InviteMembersModal', () => { ...@@ -476,19 +464,6 @@ describe('InviteMembersModal', () => {
); );
expect(findMembersSelect().props('validationState')).toBe(false); expect(findMembersSelect().props('validationState')).toBe(false);
}); });
it('displays an access_level error message received for the existing user', async () => {
mockMembersApi(httpStatus.BAD_REQUEST, membersApiResponse.SINGLE_USER_ACCESS_LEVEL);
clickInviteButton();
await waitForPromises();
expect(membersFormGroupInvalidFeedback()).toBe(
'should be greater than or equal to Owner inherited membership from group Gitlab Org',
);
expect(findMembersSelect().props('validationState')).toBe(false);
});
}); });
}); });
...@@ -509,7 +484,7 @@ describe('InviteMembersModal', () => { ...@@ -509,7 +484,7 @@ describe('InviteMembersModal', () => {
await triggerMembersTokenSelect([user3]); await triggerMembersTokenSelect([user3]);
wrapper.vm.$toast = { show: jest.fn() }; wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData }); jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
}); });
describe('when triggered from regular mounting', () => { describe('when triggered from regular mounting', () => {
...@@ -517,8 +492,8 @@ describe('InviteMembersModal', () => { ...@@ -517,8 +492,8 @@ describe('InviteMembersModal', () => {
clickInviteButton(); clickInviteButton();
}); });
it('calls Api inviteGroupMembersByEmail with the correct params', () => { it('calls Api inviteGroupMembers with the correct params', () => {
expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(propsData.id, postData); expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
}); });
it('displays the successful toastMessage', () => { it('displays the successful toastMessage', () => {
...@@ -558,20 +533,8 @@ describe('InviteMembersModal', () => { ...@@ -558,20 +533,8 @@ describe('InviteMembersModal', () => {
expect(findModal().props('actionPrimary').attributes.loading).toBe(false); expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
}); });
it('displays the successful toast message when email has already been invited', async () => {
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN);
wrapper.vm.$toast = { show: jest.fn() };
clickInviteButton();
await waitForPromises();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added');
expect(findMembersSelect().props('validationState')).toBe(null);
});
it('displays the first error message when multiple emails return a restricted error message', async () => { it('displays the first error message when multiple emails return a restricted error message', async () => {
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_EMAIL_RESTRICTED); mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
clickInviteButton(); clickInviteButton();
...@@ -618,19 +581,17 @@ describe('InviteMembersModal', () => { ...@@ -618,19 +581,17 @@ describe('InviteMembersModal', () => {
format: 'json', format: 'json',
tasks_to_be_done: [], tasks_to_be_done: [],
tasks_project_id: '', tasks_project_id: '',
user_id: '1',
email: 'email@example.com',
}; };
const emailPostData = { ...postData, email: 'email@example.com' };
const idPostData = { ...postData, user_id: '1' };
describe('when invites are sent successfully', () => { describe('when invites are sent successfully', () => {
beforeEach(async () => { beforeEach(async () => {
createComponent(); createComponent();
await triggerMembersTokenSelect([user1, user3]); await triggerMembersTokenSelect([user1, user3]);
wrapper.vm.$toast = { show: jest.fn() }; wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData }); jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
}); });
describe('when triggered from regular mounting', () => { describe('when triggered from regular mounting', () => {
...@@ -638,12 +599,8 @@ describe('InviteMembersModal', () => { ...@@ -638,12 +599,8 @@ describe('InviteMembersModal', () => {
clickInviteButton(); clickInviteButton();
}); });
it('calls Api inviteGroupMembersByEmail with the correct params', () => { it('calls Api inviteGroupMembers with the correct params', () => {
expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(propsData.id, emailPostData); expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
});
it('calls Api addGroupMembersByUserId with the correct params', () => {
expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(propsData.id, idPostData);
}); });
it('displays the successful toastMessage', () => { it('displays the successful toastMessage', () => {
...@@ -656,12 +613,8 @@ describe('InviteMembersModal', () => { ...@@ -656,12 +613,8 @@ describe('InviteMembersModal', () => {
clickInviteButton(); clickInviteButton();
expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(propsData.id, { expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, {
...emailPostData, ...postData,
invite_source: '_invite_source_',
});
expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(propsData.id, {
...idPostData,
invite_source: '_invite_source_', invite_source: '_invite_source_',
}); });
}); });
...@@ -674,7 +627,6 @@ describe('InviteMembersModal', () => { ...@@ -674,7 +627,6 @@ describe('InviteMembersModal', () => {
await triggerMembersTokenSelect([user1, user3]); await triggerMembersTokenSelect([user1, user3]);
mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID); mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
mockMembersApi(httpStatus.OK, '200 OK');
clickInviteButton(); clickInviteButton();
}); });
...@@ -693,7 +645,7 @@ describe('InviteMembersModal', () => { ...@@ -693,7 +645,7 @@ describe('InviteMembersModal', () => {
await triggerMembersTokenSelect([user3]); await triggerMembersTokenSelect([user3]);
wrapper.vm.$toast = { show: jest.fn() }; wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({}); jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({});
}); });
it('tracks the view for learn_gitlab source', () => { it('tracks the view for learn_gitlab source', () => {
......
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
find(member_dropdown_selector).set(name) Array.wrap(names).each do |name|
find(member_dropdown_selector).set(name)
wait_for_requests
click_button name
end
wait_for_requests
click_button name
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