From 2e1496cbd0e4169503dc6165c1d09f57237757db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Sanz=20Garc=C3=ADa?= Date: Tue, 28 Nov 2023 16:42:36 +0100 Subject: [PATCH 1/4] Replaced query for SAML users and service accounts In the group and project member pages, the invite user modal is used to query and filter users. When the top-level group has a SAML provider enabled and the option `Enforce SSO-only authentication for web activity for this group` only SAML users are returned. Following https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134570, we replace the user API endpoint (`/api/v4/users.json`) for the new group user API endpoint (`/api/v4/groups/[groupId]/users.json`) to get a list users that have a SAML identity connected to the group, or service accounts created by the group or subgroups Related to https://gitlab.com/gitlab-org/gitlab/-/issues/424505 --- app/assets/javascripts/api/user_api.js | 20 +++++++++++++ .../components/invite_members_modal.vue | 7 +---- .../components/members_token_select.vue | 23 +++++++++------ .../init_invite_members_modal.js | 1 - spec/frontend/api/user_api_spec.js | 28 +++++++++++++++++++ .../components/members_token_select_spec.js | 24 ++++++---------- 6 files changed, 71 insertions(+), 32 deletions(-) diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js index c056b42b5b6df4..880cf20ef551bb 100644 --- a/app/assets/javascripts/api/user_api.js +++ b/app/assets/javascripts/api/user_api.js @@ -4,6 +4,7 @@ import { buildApiUrl } from './api_utils'; const USER_COUNTS_PATH = '/api/:version/user_counts'; const USERS_PATH = '/api/:version/users.json'; +const USERS_SAML_PATH = '/api/:version/groups/:id/users.json'; const USER_PATH = '/api/:version/users/:id'; const USER_STATUS_PATH = '/api/:version/users/:id/status'; const USER_PROJECTS_PATH = '/api/:version/users/:id/projects'; @@ -25,6 +26,25 @@ export function getUsers(query, options) { }); } +/** + * Returns a list of SAML users and service accounts that contains the query string. + * If the query string is less than 3 characters it returns an empty list. + * + * @param {string} query - query string to search + * @param {string} groupId -- top-level group id + * @param {object} options + */ +export function getSAMLUsers(query, groupId, options) { + const url = buildApiUrl(USERS_SAML_PATH).replace(':id', groupId); + return axios.get(url, { + params: { + search: query, + per_page: DEFAULT_PER_PAGE, + ...options, + }, + }); +} + export function getUser(id, options) { const url = buildApiUrl(USER_PATH).replace(':id', encodeURIComponent(id)); return axios.get(url, { diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index bcf594a7b1ce98..ca361e4a873a6a 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -99,11 +99,6 @@ export default { required: false, default: USERS_FILTER_ALL, }, - filterId: { - type: Number, - required: false, - default: null, - }, fullPath: { type: String, required: true, @@ -515,7 +510,7 @@ export default { :input-id="inputId" :exception-state="exceptionState" :users-filter="usersFilter" - :filter-id="filterId" + :root-group-id="rootId" :invalid-members="invalidMembers" @clear="clearValidation" @token-remove="removeToken" diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index b29755545f23bf..fab13f1aacf096 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -2,7 +2,7 @@ import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlIcon, GlSprintf } from '@gitlab/ui'; import { debounce, isEmpty } from 'lodash'; import { __ } from '~/locale'; -import { getUsers } from '~/rest_api'; +import { getUsers, getSAMLUsers } from '~/rest_api'; import { memberName } from '../utils/member_utils'; import { SEARCH_DELAY, @@ -45,11 +45,6 @@ export default { required: false, default: USERS_FILTER_ALL, }, - filterId: { - type: Number, - required: false, - default: null, - }, invalidMembers: { type: Object, required: true, @@ -59,6 +54,10 @@ export default { required: false, default: '', }, + rootGroupId: { + type: String, + required: true, + }, }, data() { return { @@ -88,8 +87,8 @@ export default { queryOptions() { if (this.usersFilter === USERS_FILTER_SAML_PROVIDER_ID) { return { - saml_provider_id: this.filterId, - ...this.$options.defaultQueryOptions, + include_saml_users: true, + include_service_accounts: true, }; } return this.$options.defaultQueryOptions; @@ -134,7 +133,13 @@ export default { })); }, retrieveUsers: debounce(function debouncedRetrieveUsers() { - return getUsers(this.query, this.queryOptions) + let request; + if (this.usersFilter === USERS_FILTER_SAML_PROVIDER_ID) { + request = getSAMLUsers(this.query, this.rootGroupId, this.queryOptions); + } else { + request = getUsers(this.query, this.queryOptions); + } + return request .then((response) => { this.users = response.data.map((token) => ({ id: token.id, diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js index bd291ecc90f336..08d9c687eb4245 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -38,7 +38,6 @@ export default (function initInviteMembersModal() { defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10), defaultMemberRoleId: parseInt(el.dataset.defaultMemberRoleId, 10) || null, usersFilter: el.dataset.usersFilter, - filterId: parseInt(el.dataset.filterId, 10), usersLimitDataset: convertObjectPropsToCamelCase( JSON.parse(el.dataset.usersLimitDataset || '{}'), ), diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js index a6e08e1cf4b5a4..b450b40b2dbc70 100644 --- a/spec/frontend/api/user_api_spec.js +++ b/spec/frontend/api/user_api_spec.js @@ -4,6 +4,8 @@ import projects from 'test_fixtures/api/users/projects/get.json'; import followers from 'test_fixtures/api/users/followers/get.json'; import following from 'test_fixtures/api/users/following/get.json'; import { + getUsers, + getSAMLUsers, followUser, unfollowUser, associationsCount, @@ -36,6 +38,32 @@ describe('~/api/user_api', () => { axiosMock.resetHistory(); }); + describe('getUsers', () => { + it('calls correct URL with expected query parameters', async () => { + const expectedUrl = '/api/v4/users.json'; + axiosMock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK); + + await getUsers('den', { without_project_bots: true }); + + const { url, params } = axiosMock.history.get[0]; + expect(url).toBe(expectedUrl); + expect(params).toMatchObject({ search: 'den', without_project_bots: true }); + }); + }); + + describe('getSAMLUsers', () => { + it('calls correct URL with expected query parameters', async () => { + const expectedUrl = '/api/v4/groups/34/users.json'; + axiosMock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK); + + await getSAMLUsers('den', '34', { include_service_accounts: true }); + + const { url, params } = axiosMock.history.get[0]; + expect(url).toBe(expectedUrl); + expect(params).toMatchObject({ search: 'den', include_service_accounts: true }); + }); + }); + describe('followUser', () => { it('calls correct URL and returns expected response', async () => { const expectedUrl = '/api/v4/users/1/follow'; diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js index 5e36cfe915ad66..82c91bfea7daa9 100644 --- a/spec/frontend/invite_members/components/members_token_select_spec.js +++ b/spec/frontend/invite_members/components/members_token_select_spec.js @@ -9,6 +9,7 @@ import { VALID_TOKEN_BACKGROUND, INVALID_TOKEN_BACKGROUND } from '~/invite_membe const label = 'testgroup'; const placeholder = 'Search for a member'; +const rootGroupId = '31'; const user1 = { id: 1, name: 'John Smith', username: 'one_1', avatar_url: '' }; const user2 = { id: 2, name: 'Jane Doe', username: 'two_2', avatar_url: '' }; const allUsers = [user1, user2]; @@ -20,6 +21,7 @@ const createComponent = (props) => { ariaLabelledby: label, invalidMembers: {}, placeholder, + rootGroupId, ...props, }, stubs: { @@ -216,31 +218,21 @@ describe('MembersTokenSelect', () => { }); }); - describe('when component is mounted for a group using a saml provider', () => { + describe('when component is mounted for a group using a SAML provider', () => { const searchParam = 'name'; - const samlProviderId = 123; - let resolveApiRequest; beforeEach(() => { - jest.spyOn(UserApi, 'getUsers').mockImplementation( - () => - new Promise((resolve) => { - resolveApiRequest = resolve; - }), - ); + jest.spyOn(UserApi, 'getSAMLUsers').mockResolvedValue({ data: allUsers }); - wrapper = createComponent({ filterId: samlProviderId, usersFilter: 'saml_provider_id' }); + wrapper = createComponent({ usersFilter: 'saml_provider_id' }); findTokenSelector().vm.$emit('text-input', searchParam); }); it('calls the API with the saml provider ID param', () => { - resolveApiRequest({ data: allUsers }); - - expect(UserApi.getUsers).toHaveBeenCalledWith(searchParam, { - active: true, - without_project_bots: true, - saml_provider_id: samlProviderId, + expect(UserApi.getSAMLUsers).toHaveBeenCalledWith(searchParam, rootGroupId, { + include_saml_users: true, + include_service_accounts: true, }); }); }); -- GitLab From f045557ef937cf4f698c349086b020bf3bb43457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Sanz=20Garc=C3=ADa?= Date: Thu, 30 Nov 2023 12:09:49 +0100 Subject: [PATCH 2/4] Apply suggestions from review * Added Sentry reporting and test * Improvements in the logic of fetching the users --- .../components/members_token_select.vue | 37 ++++++++++--------- .../components/members_token_select_spec.js | 16 ++++++++ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index fab13f1aacf096..5aaaf5b430d863 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -3,6 +3,7 @@ import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlIcon, GlSprintf } from '@ import { debounce, isEmpty } from 'lodash'; import { __ } from '~/locale'; import { getUsers, getSAMLUsers } from '~/rest_api'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { memberName } from '../utils/member_utils'; import { SEARCH_DELAY, @@ -132,25 +133,27 @@ export default { class: this.tokenClass(token), })); }, - retrieveUsers: debounce(function debouncedRetrieveUsers() { - let request; + retrieveUsersRequest() { if (this.usersFilter === USERS_FILTER_SAML_PROVIDER_ID) { - request = getSAMLUsers(this.query, this.rootGroupId, this.queryOptions); - } else { - request = getUsers(this.query, this.queryOptions); + return getSAMLUsers(this.query, this.rootGroupId, this.queryOptions); } - return request - .then((response) => { - this.users = response.data.map((token) => ({ - id: token.id, - name: token.name, - username: token.username, - avatar_url: token.avatar_url, - })); - }) - .finally(() => { - this.loading = false; - }); + + return getUsers(this.query, this.queryOptions); + }, + retrieveUsers: debounce(async function debouncedRetrieveUsers() { + try { + const { data } = await this.retrieveUsersRequest(); + this.users = data.map((token) => ({ + id: token.id, + name: token.name, + username: token.username, + avatar_url: token.avatar_url, + })); + } catch (error) { + Sentry.captureException(error); + } + + this.loading = false; }, SEARCH_DELAY), tokenClass(token) { if (this.hasError(token)) { diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js index 82c91bfea7daa9..2ebbf8dfa2b071 100644 --- a/spec/frontend/invite_members/components/members_token_select_spec.js +++ b/spec/frontend/invite_members/components/members_token_select_spec.js @@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import * as UserApi from '~/api/user_api'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; import { VALID_TOKEN_BACKGROUND, INVALID_TOKEN_BACKGROUND } from '~/invite_members/constants'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; const label = 'testgroup'; const placeholder = 'Search for a member'; @@ -169,6 +170,21 @@ describe('MembersTokenSelect', () => { }); }); + describe('when API search fails', () => { + beforeEach(() => { + jest.spyOn(Sentry, 'captureException'); + jest.spyOn(UserApi, 'getUsers').mockRejectedValue('error'); + }); + + it('reports to sentry', async () => { + tokenSelector.vm.$emit('text-input', 'Den'); + + await waitForPromises(); + + expect(Sentry.captureException).toHaveBeenCalledWith('error'); + }); + }); + it('allows tab to function as enter', () => { tokenSelector.vm.$emit('text-input', 'username'); -- GitLab From eafe18694c8205d6d94325df10c242b0663bda14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Sanz=20Garc=C3=ADa?= Date: Fri, 8 Dec 2023 09:30:52 +0100 Subject: [PATCH 3/4] Rename `getSAMLUsers` to `getGroupUsers` --- app/assets/javascripts/api/user_api.js | 2 +- .../invite_members/components/members_token_select.vue | 4 ++-- spec/frontend/api/user_api_spec.js | 4 ++-- .../invite_members/components/members_token_select_spec.js | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js index 880cf20ef551bb..302de9760800c1 100644 --- a/app/assets/javascripts/api/user_api.js +++ b/app/assets/javascripts/api/user_api.js @@ -34,7 +34,7 @@ export function getUsers(query, options) { * @param {string} groupId -- top-level group id * @param {object} options */ -export function getSAMLUsers(query, groupId, options) { +export function getGroupUsers(query, groupId, options) { const url = buildApiUrl(USERS_SAML_PATH).replace(':id', groupId); return axios.get(url, { params: { diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index 5aaaf5b430d863..82ff73fabac1ed 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -2,7 +2,7 @@ import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlIcon, GlSprintf } from '@gitlab/ui'; import { debounce, isEmpty } from 'lodash'; import { __ } from '~/locale'; -import { getUsers, getSAMLUsers } from '~/rest_api'; +import { getUsers, getGroupUsers } from '~/rest_api'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { memberName } from '../utils/member_utils'; import { @@ -135,7 +135,7 @@ export default { }, retrieveUsersRequest() { if (this.usersFilter === USERS_FILTER_SAML_PROVIDER_ID) { - return getSAMLUsers(this.query, this.rootGroupId, this.queryOptions); + return getGroupUsers(this.query, this.rootGroupId, this.queryOptions); } return getUsers(this.query, this.queryOptions); diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js index b450b40b2dbc70..aeddf6b9ae135f 100644 --- a/spec/frontend/api/user_api_spec.js +++ b/spec/frontend/api/user_api_spec.js @@ -5,7 +5,7 @@ import followers from 'test_fixtures/api/users/followers/get.json'; import following from 'test_fixtures/api/users/following/get.json'; import { getUsers, - getSAMLUsers, + getGroupUsers, followUser, unfollowUser, associationsCount, @@ -56,7 +56,7 @@ describe('~/api/user_api', () => { const expectedUrl = '/api/v4/groups/34/users.json'; axiosMock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK); - await getSAMLUsers('den', '34', { include_service_accounts: true }); + await getGroupUsers('den', '34', { include_service_accounts: true }); const { url, params } = axiosMock.history.get[0]; expect(url).toBe(expectedUrl); diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js index 2ebbf8dfa2b071..098e492ff6e4b6 100644 --- a/spec/frontend/invite_members/components/members_token_select_spec.js +++ b/spec/frontend/invite_members/components/members_token_select_spec.js @@ -238,7 +238,7 @@ describe('MembersTokenSelect', () => { const searchParam = 'name'; beforeEach(() => { - jest.spyOn(UserApi, 'getSAMLUsers').mockResolvedValue({ data: allUsers }); + jest.spyOn(UserApi, 'getGroupUsers').mockResolvedValue({ data: allUsers }); wrapper = createComponent({ usersFilter: 'saml_provider_id' }); @@ -246,7 +246,7 @@ describe('MembersTokenSelect', () => { }); it('calls the API with the saml provider ID param', () => { - expect(UserApi.getSAMLUsers).toHaveBeenCalledWith(searchParam, rootGroupId, { + expect(UserApi.getGroupUsers).toHaveBeenCalledWith(searchParam, rootGroupId, { include_saml_users: true, include_service_accounts: true, }); -- GitLab From 8b9f1f65a25cc182b800d411833d85b053f991ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Sanz=20Garc=C3=ADa?= Date: Fri, 8 Dec 2023 10:25:37 +0100 Subject: [PATCH 4/4] Add active parameter --- .../components/invite_members_modal.vue | 6 ++++ .../components/members_token_select.vue | 16 +++++++++- .../init_invite_members_modal.js | 1 + .../development/group_user_saml.yml | 8 +++++ lib/gitlab/gon_helper.rb | 1 + .../components/members_token_select_spec.js | 31 +++++++++++++++++-- 6 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 config/feature_flags/development/group_user_saml.yml diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index ca361e4a873a6a..dead90eeb71ff0 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -99,6 +99,11 @@ export default { required: false, default: USERS_FILTER_ALL, }, + filterId: { + type: Number, + required: false, + default: null, + }, fullPath: { type: String, required: true, @@ -510,6 +515,7 @@ export default { :input-id="inputId" :exception-state="exceptionState" :users-filter="usersFilter" + :filter-id="filterId" :root-group-id="rootId" :invalid-members="invalidMembers" @clear="clearValidation" diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index 82ff73fabac1ed..0e3f2890b29898 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -4,6 +4,7 @@ import { debounce, isEmpty } from 'lodash'; import { __ } from '~/locale'; import { getUsers, getGroupUsers } from '~/rest_api'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { memberName } from '../utils/member_utils'; import { SEARCH_DELAY, @@ -21,6 +22,7 @@ export default { GlIcon, GlSprintf, }, + mixins: [glFeatureFlagsMixin()], props: { canUseEmailToken: { type: Boolean, @@ -46,6 +48,11 @@ export default { required: false, default: USERS_FILTER_ALL, }, + filterId: { + type: Number, + required: false, + default: null, + }, invalidMembers: { type: Object, required: true, @@ -87,7 +94,14 @@ export default { }, queryOptions() { if (this.usersFilter === USERS_FILTER_SAML_PROVIDER_ID) { + if (!this.glFeatures.groupUserSaml) { + return { + saml_provider_id: this.filterId, + ...this.$options.defaultQueryOptions, + }; + } return { + active: true, include_saml_users: true, include_service_accounts: true, }; @@ -134,7 +148,7 @@ export default { })); }, retrieveUsersRequest() { - if (this.usersFilter === USERS_FILTER_SAML_PROVIDER_ID) { + if (this.usersFilter === USERS_FILTER_SAML_PROVIDER_ID && this.glFeatures.groupUserSaml) { return getGroupUsers(this.query, this.rootGroupId, this.queryOptions); } diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js index 08d9c687eb4245..bd291ecc90f336 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -38,6 +38,7 @@ export default (function initInviteMembersModal() { defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10), defaultMemberRoleId: parseInt(el.dataset.defaultMemberRoleId, 10) || null, usersFilter: el.dataset.usersFilter, + filterId: parseInt(el.dataset.filterId, 10), usersLimitDataset: convertObjectPropsToCamelCase( JSON.parse(el.dataset.usersLimitDataset || '{}'), ), diff --git a/config/feature_flags/development/group_user_saml.yml b/config/feature_flags/development/group_user_saml.yml new file mode 100644 index 00000000000000..f3a03aad18f77d --- /dev/null +++ b/config/feature_flags/development/group_user_saml.yml @@ -0,0 +1,8 @@ +--- +name: group_user_saml +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138075 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/434464 +milestone: '16.7' +type: development +group: group::authentication +default_enabled: false diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index b630eb13a606a6..0a49a2c750553b 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -82,6 +82,7 @@ def add_gon_variables push_frontend_feature_flag(:remove_monitor_metrics) push_frontend_feature_flag(:custom_emoji) push_frontend_feature_flag(:encoding_logs_tree) + push_frontend_feature_flag(:group_user_saml) end # Exposes the state of a feature flag to the frontend code. diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js index 098e492ff6e4b6..a2b21367388884 100644 --- a/spec/frontend/invite_members/components/members_token_select_spec.js +++ b/spec/frontend/invite_members/components/members_token_select_spec.js @@ -16,7 +16,7 @@ const user2 = { id: 2, name: 'Jane Doe', username: 'two_2', avatar_url: '' }; const allUsers = [user1, user2]; const handleEnterSpy = jest.fn(); -const createComponent = (props) => { +const createComponent = (props = {}, glFeatures = {}) => { return shallowMount(MembersTokenSelect, { propsData: { ariaLabelledby: label, @@ -25,6 +25,7 @@ const createComponent = (props) => { rootGroupId, ...props, }, + provide: { glFeatures }, stubs: { GlTokenSelector: stubComponent(GlTokenSelector, { methods: { @@ -240,16 +241,40 @@ describe('MembersTokenSelect', () => { beforeEach(() => { jest.spyOn(UserApi, 'getGroupUsers').mockResolvedValue({ data: allUsers }); - wrapper = createComponent({ usersFilter: 'saml_provider_id' }); + wrapper = createComponent({ usersFilter: 'saml_provider_id' }, { groupUserSaml: true }); findTokenSelector().vm.$emit('text-input', searchParam); }); - it('calls the API with the saml provider ID param', () => { + it('calls the group API with correct parameters', () => { expect(UserApi.getGroupUsers).toHaveBeenCalledWith(searchParam, rootGroupId, { + active: true, include_saml_users: true, include_service_accounts: true, }); }); }); + + describe('when group_user_saml feature flag is disabled', () => { + describe('when component is mounted for a group using a SAML provider', () => { + const searchParam = 'name'; + const samlProviderId = 123; + + beforeEach(() => { + jest.spyOn(UserApi, 'getUsers').mockResolvedValue({ data: allUsers }); + + wrapper = createComponent({ filterId: samlProviderId, usersFilter: 'saml_provider_id' }); + + findTokenSelector().vm.$emit('text-input', searchParam); + }); + + it('calls the API with the saml provider ID param', () => { + expect(UserApi.getUsers).toHaveBeenCalledWith(searchParam, { + active: true, + without_project_bots: true, + saml_provider_id: samlProviderId, + }); + }); + }); + }); }); -- GitLab