summaryrefslogtreecommitdiff
path: root/spec/frontend/invite_members/components/invite_members_modal_spec.js
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/invite_members/components/invite_members_modal_spec.js')
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js295
1 files changed, 219 insertions, 76 deletions
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index eabbea84234..b828b5d8a04 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -1,11 +1,27 @@
-import { GlDropdown, GlDropdownItem, GlDatepicker, GlSprintf, GlLink, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDatepicker,
+ GlFormGroup,
+ GlSprintf,
+ GlLink,
+ GlModal,
+} from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
+import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import { INVITE_MEMBERS_IN_COMMENT } from '~/invite_members/constants';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
+import { apiPaths, membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses';
+
+let wrapper;
+let mock;
jest.mock('~/experimentation/experiment_tracking');
@@ -26,10 +42,16 @@ const user3 = {
username: 'one_2',
avatar_url: '',
};
+const user4 = {
+ id: 'user-defined-token',
+ name: 'email4@example.com',
+ username: 'one_4',
+ avatar_url: '',
+};
const sharedGroup = { id: '981' };
const createComponent = (data = {}, props = {}) => {
- return shallowMount(InviteMembersModal, {
+ wrapper = shallowMountExtended(InviteMembersModal, {
propsData: {
id,
name,
@@ -51,46 +73,56 @@ const createComponent = (data = {}, props = {}) => {
GlDropdown: true,
GlDropdownItem: true,
GlSprintf,
+ GlFormGroup: stubComponent(GlFormGroup, {
+ props: ['state', 'invalidFeedback'],
+ }),
},
});
};
const createInviteMembersToProjectWrapper = () => {
- return createComponent({ inviteeType: 'members' }, { isProject: true });
+ createComponent({ inviteeType: 'members' }, { isProject: true });
};
const createInviteMembersToGroupWrapper = () => {
- return createComponent({ inviteeType: 'members' }, { isProject: false });
+ createComponent({ inviteeType: 'members' }, { isProject: false });
};
const createInviteGroupToProjectWrapper = () => {
- return createComponent({ inviteeType: 'group' }, { isProject: true });
+ createComponent({ inviteeType: 'group' }, { isProject: true });
};
const createInviteGroupToGroupWrapper = () => {
- return createComponent({ inviteeType: 'group' }, { isProject: false });
+ createComponent({ inviteeType: 'group' }, { isProject: false });
};
-describe('InviteMembersModal', () => {
- let wrapper;
+beforeEach(() => {
+ gon.api_version = 'v4';
+ mock = new MockAdapter(axios);
+});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
+afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ mock.restore();
+});
+describe('InviteMembersModal', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem);
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
const findLink = () => wrapper.findComponent(GlLink);
const findIntroText = () => wrapper.find({ ref: 'introText' }).text();
- const findCancelButton = () => wrapper.findComponent({ ref: 'cancelButton' });
- const findInviteButton = () => wrapper.findComponent({ ref: 'inviteButton' });
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+ const findInviteButton = () => wrapper.findByTestId('invite-button');
const clickInviteButton = () => findInviteButton().vm.$emit('click');
+ const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
+ const membersFormGroupInvalidFeedback = () => findMembersFormGroup().props('invalidFeedback');
+ const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
describe('rendering the modal', () => {
beforeEach(() => {
- wrapper = createComponent();
+ createComponent();
});
it('renders the modal with the correct title', () => {
@@ -132,7 +164,7 @@ describe('InviteMembersModal', () => {
describe('when inviting to a project', () => {
describe('when inviting members', () => {
it('includes the correct invitee, type, and formatted name', () => {
- wrapper = createInviteMembersToProjectWrapper();
+ createInviteMembersToProjectWrapper();
expect(findIntroText()).toBe("You're inviting members to the test name project.");
});
@@ -140,7 +172,7 @@ describe('InviteMembersModal', () => {
describe('when sharing with a group', () => {
it('includes the correct invitee, type, and formatted name', () => {
- wrapper = createInviteGroupToProjectWrapper();
+ createInviteGroupToProjectWrapper();
expect(findIntroText()).toBe("You're inviting a group to the test name project.");
});
@@ -150,7 +182,7 @@ describe('InviteMembersModal', () => {
describe('when inviting to a group', () => {
describe('when inviting members', () => {
it('includes the correct invitee, type, and formatted name', () => {
- wrapper = createInviteMembersToGroupWrapper();
+ createInviteMembersToGroupWrapper();
expect(findIntroText()).toBe("You're inviting members to the test name group.");
});
@@ -158,7 +190,7 @@ describe('InviteMembersModal', () => {
describe('when sharing with a group', () => {
it('includes the correct invitee, type, and formatted name', () => {
- wrapper = createInviteGroupToGroupWrapper();
+ createInviteGroupToGroupWrapper();
expect(findIntroText()).toBe("You're inviting a group to the test name group.");
});
@@ -167,22 +199,30 @@ describe('InviteMembersModal', () => {
});
describe('submitting the invite form', () => {
- const apiErrorMessage = 'Member already exists';
+ const mockMembersApi = (code, data) => {
+ mock.onPost(apiPaths.GROUPS_MEMBERS).reply(code, data);
+ };
+ const mockInvitationsApi = (code, data) => {
+ mock.onPost(apiPaths.GROUPS_INVITATIONS).reply(code, data);
+ };
+
+ const expectedEmailRestrictedError =
+ "email 'email@example.com' does not match the allowed domains: example1.org";
+ const expectedSyntaxError = 'email contains an invalid email address';
describe('when inviting an existing user to group by user ID', () => {
const postData = {
- user_id: '1',
+ user_id: '1,2',
access_level: defaultAccessLevel,
expires_at: undefined,
invite_source: inviteSource,
format: 'json',
};
- describe('when invites are sent successfully', () => {
+ describe('when member is added successfully', () => {
beforeEach(() => {
- wrapper = createInviteMembersToGroupWrapper();
+ createComponent({ newUsersToInvite: [user1, user2] });
- wrapper.setData({ newUsersToInvite: [user1] });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
@@ -190,54 +230,102 @@ describe('InviteMembersModal', () => {
clickInviteButton();
});
- it('calls Api addGroupMembersByUserId with the correct params', () => {
+ it('calls Api addGroupMembersByUserId with the correct params', async () => {
+ await waitForPromises;
+
expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, postData);
});
- it('displays the successful toastMessage', () => {
+ it('displays the successful toastMessage', async () => {
+ await waitForPromises;
+
expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
});
});
- describe('when the invite received an api error message', () => {
+ describe('when member is not added successfully', () => {
beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user1] });
+ createInviteMembersToGroupWrapper();
- wrapper.vm.$toast = { show: jest.fn() };
- jest
- .spyOn(Api, 'addGroupMembersByUserId')
- .mockRejectedValue({ response: { data: { message: apiErrorMessage } } });
- jest.spyOn(wrapper.vm, 'showToastMessageError');
+ wrapper.setData({ newUsersToInvite: [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(findMembersFormGroup().props('state')).toBe(false);
+ expect(findMembersSelect().props('validationState')).toBe(false);
});
- it('displays the apiErrorMessage in the toastMessage', 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);
+
+ clickInviteButton();
+
await waitForPromises();
- expect(wrapper.vm.showToastMessageError).toHaveBeenCalledWith({
- response: { data: { message: apiErrorMessage } },
- });
+ expect(membersFormGroupInvalidFeedback()).toBe('Member already exists');
+ expect(findMembersFormGroup().props('state')).toBe(false);
+ expect(findMembersSelect().props('validationState')).toBe(false);
+
+ findMembersSelect().vm.$emit('clear');
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersFormGroup().props('state')).not.toBe(false);
+ expect(findMembersSelect().props('validationState')).not.toBe(false);
});
- });
- describe('when any invite failed for any other reason', () => {
- beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user1, user2] });
+ it('displays the generic error for http server error', async () => {
+ mockMembersApi(httpStatus.INTERNAL_SERVER_ERROR, 'Request failed with status code 500');
- wrapper.vm.$toast = { show: jest.fn() };
- jest
- .spyOn(Api, 'addGroupMembersByUserId')
- .mockRejectedValue({ response: { data: { success: false } } });
- jest.spyOn(wrapper.vm, 'showToastMessageError');
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe('Something went wrong');
+ });
+
+ it('displays the restricted user api message for response with bad request', async () => {
+ mockMembersApi(httpStatus.BAD_REQUEST, membersApiResponse.SINGLE_USER_RESTRICTED);
clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedEmailRestrictedError);
});
- it('displays the generic error toastMessage', 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);
+
+ clickInviteButton();
+
await waitForPromises();
- expect(wrapper.vm.showToastMessageError).toHaveBeenCalled();
+ expect(membersFormGroupInvalidFeedback()).toBe(
+ "root: User email 'admin@example.com' does not match the allowed domain of example2.com",
+ );
+ 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);
});
});
});
@@ -253,7 +341,7 @@ describe('InviteMembersModal', () => {
describe('when invites are sent successfully', () => {
beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user3] });
+ createComponent({ newUsersToInvite: [user3] });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
@@ -271,23 +359,84 @@ describe('InviteMembersModal', () => {
});
});
- describe('when any invite failed for any reason', () => {
+ describe('when invites are not sent successfully', () => {
beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user1, user2] });
+ createInviteMembersToGroupWrapper();
+
+ wrapper.setData({ newUsersToInvite: [user3] });
+ });
+
+ it('displays the api error for invalid email syntax', async () => {
+ mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
+
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
+ expect(findMembersSelect().props('validationState')).toBe(false);
+ });
+ it('displays the restricted email error when restricted email is invited', async () => {
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
+
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toContain(expectedEmailRestrictedError);
+ expect(findMembersSelect().props('validationState')).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() };
- jest
- .spyOn(Api, 'addGroupMembersByUserId')
- .mockRejectedValue({ response: { data: { success: false } } });
- jest.spyOn(wrapper.vm, 'showToastMessageError');
+ jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
+
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
+ expect(findMembersSelect().props('validationState')).toBe(null);
+ });
+
+ it('displays the first error message when multiple emails return a restricted error message', async () => {
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_EMAIL_RESTRICTED);
clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toContain(expectedEmailRestrictedError);
+ expect(findMembersSelect().props('validationState')).toBe(false);
+ });
+
+ it('displays the invalid syntax error for bad request', async () => {
+ mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.ERROR_EMAIL_INVALID);
+
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
+ expect(findMembersSelect().props('validationState')).toBe(false);
});
+ });
+
+ describe('when multiple emails are invited at the same time', () => {
+ it('displays the invalid syntax error if one of the emails is invalid', async () => {
+ createInviteMembersToGroupWrapper();
+
+ wrapper.setData({ newUsersToInvite: [user3, user4] });
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.ERROR_EMAIL_INVALID);
+
+ clickInviteButton();
- it('displays the generic error toastMessage', async () => {
await waitForPromises();
- expect(wrapper.vm.showToastMessageError).toHaveBeenCalled();
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
+ expect(findMembersSelect().props('validationState')).toBe(false);
});
});
});
@@ -305,7 +454,7 @@ describe('InviteMembersModal', () => {
describe('when invites are sent successfully', () => {
beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user1, user3] });
+ createComponent({ newUsersToInvite: [user1, user3] });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
@@ -350,24 +499,20 @@ describe('InviteMembersModal', () => {
describe('when any invite failed for any reason', () => {
beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user1, user3] });
+ createInviteMembersToGroupWrapper();
- wrapper.vm.$toast = { show: jest.fn() };
-
- jest
- .spyOn(Api, 'inviteGroupMembersByEmail')
- .mockRejectedValue({ response: { data: { success: false } } });
+ wrapper.setData({ newUsersToInvite: [user1, user3] });
- jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
- jest.spyOn(wrapper.vm, 'showToastMessageError');
+ mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
+ mockMembersApi(httpStatus.OK, '200 OK');
clickInviteButton();
});
- it('displays the generic error toastMessage', async () => {
+ it('displays the first error message', async () => {
await waitForPromises();
- expect(wrapper.vm.showToastMessageError).toHaveBeenCalled();
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
});
});
});
@@ -382,7 +527,7 @@ describe('InviteMembersModal', () => {
};
beforeEach(() => {
- wrapper = createComponent({ groupToBeSharedWith: sharedGroup });
+ createComponent({ groupToBeSharedWith: sharedGroup });
wrapper.setData({ inviteeType: 'group' });
wrapper.vm.$toast = { show: jest.fn() };
@@ -403,7 +548,7 @@ describe('InviteMembersModal', () => {
describe('when sharing the group fails', () => {
beforeEach(() => {
- wrapper = createComponent({ groupToBeSharedWith: sharedGroup });
+ createComponent({ groupToBeSharedWith: sharedGroup });
wrapper.setData({ inviteeType: 'group' });
wrapper.vm.$toast = { show: jest.fn() };
@@ -412,22 +557,20 @@ describe('InviteMembersModal', () => {
.spyOn(Api, 'groupShareWithGroup')
.mockRejectedValue({ response: { data: { success: false } } });
- jest.spyOn(wrapper.vm, 'showToastMessageError');
-
clickInviteButton();
});
- it('displays the generic error toastMessage', async () => {
+ it('displays the generic error message', async () => {
await waitForPromises();
- expect(wrapper.vm.showToastMessageError).toHaveBeenCalled();
+ expect(membersFormGroupInvalidFeedback()).toBe('Something went wrong');
});
});
});
describe('tracking', () => {
beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user3] });
+ createComponent({ newUsersToInvite: [user3] });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({});