diff options
-rw-r--r-- | app/controllers/search_controller.rb | 7 | ||||
-rw-r--r-- | app/helpers/projects_helper.rb | 3 | ||||
-rw-r--r-- | app/helpers/search_helper.rb | 10 | ||||
-rw-r--r-- | app/services/search/global_service.rb | 3 | ||||
-rw-r--r-- | app/services/search/group_service.rb | 6 | ||||
-rw-r--r-- | app/services/search/project_service.rb | 7 | ||||
-rw-r--r-- | app/views/search/_category.html.haml | 10 | ||||
-rw-r--r-- | app/views/search/results/_user.html.haml | 10 | ||||
-rw-r--r-- | changelogs/unreleased/feature-users-search-results.yml | 5 | ||||
-rw-r--r-- | doc/api/search.md | 69 | ||||
-rw-r--r-- | lib/api/helpers/search_helpers.rb | 6 | ||||
-rw-r--r-- | lib/api/search.rb | 13 | ||||
-rw-r--r-- | lib/gitlab/group_search_results.rb | 30 | ||||
-rw-r--r-- | lib/gitlab/project_search_results.rb | 6 | ||||
-rw-r--r-- | lib/gitlab/search_results.rb | 14 | ||||
-rw-r--r-- | locale/gitlab.pot | 3 | ||||
-rw-r--r-- | spec/features/search/user_searches_for_users_spec.rb | 83 | ||||
-rw-r--r-- | spec/lib/gitlab/group_search_results_spec.rb | 69 | ||||
-rw-r--r-- | spec/lib/gitlab/project_search_results_spec.rb | 32 | ||||
-rw-r--r-- | spec/lib/gitlab/search_results_spec.rb | 16 | ||||
-rw-r--r-- | spec/requests/api/search_spec.rb | 79 |
21 files changed, 471 insertions, 10 deletions
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 1b22907c10f..90d4bc674d9 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -29,6 +29,7 @@ class SearchController < ApplicationController @search_objects = search_service.search_objects render_commits if @scope == 'commits' + eager_load_user_status if @scope == 'users' check_single_commit_result end @@ -54,6 +55,12 @@ class SearchController < ApplicationController @search_objects = prepare_commits_for_rendering(@search_objects) end + def eager_load_user_status + return if Feature.disabled?(:users_search, default_enabled: true) + + @search_objects = @search_objects.eager_load(:status) # rubocop:disable CodeReuse/ActiveRecord + end + def check_single_commit_result if @search_results.single_commit_result? only_commit = @search_results.objects('commits').first diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 2ac8ddc5244..5496aa4908c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -364,7 +364,8 @@ module ProjectsHelper blobs: :download_code, commits: :download_code, merge_requests: :read_merge_request, - notes: [:read_merge_request, :download_code, :read_issue, :read_project_snippet] + notes: [:read_merge_request, :download_code, :read_issue, :read_project_snippet], + members: :read_project_member ) end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 0ee76a51f7d..8110377850b 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -201,4 +201,14 @@ module SearchHelper def limited_count(count, limit = 1000) count > limit ? "#{limit}+" : count end + + def search_tabs?(tab) + return false if Feature.disabled?(:users_search, default_enabled: true) + + if @project + project_search_tabs?(:members) + else + can?(current_user, :read_users_list) + end + end end diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index d6af26d949d..f711839e389 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -23,7 +23,8 @@ module Search def allowed_scopes strong_memoize(:allowed_scopes) do - %w[issues merge_requests milestones] + allowed_scopes = %w[issues merge_requests milestones] + allowed_scopes << 'users' if Feature.enabled?(:users_search, default_enabled: true) end end diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb index 34803d005e3..6f3b5f00b86 100644 --- a/app/services/search/group_service.rb +++ b/app/services/search/group_service.rb @@ -11,6 +11,12 @@ module Search @group = group end + def execute + Gitlab::GroupSearchResults.new( + current_user, projects, group, params[:search], default_project_filter: default_project_filter + ) + end + def projects return Project.none unless group return @projects if defined? @projects diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb index f223c8be103..32d5cd7ddb2 100644 --- a/app/services/search/project_service.rb +++ b/app/services/search/project_service.rb @@ -16,7 +16,12 @@ module Search end def scope - @scope ||= %w[notes issues merge_requests milestones wiki_blobs commits].delete(params[:scope]) { 'blobs' } + @scope ||= begin + allowed_scopes = %w[notes issues merge_requests milestones wiki_blobs commits] + allowed_scopes << 'users' if Feature.enabled?(:users_search, default_enabled: true) + + allowed_scopes.delete(params[:scope]) { 'blobs' } + end end end end diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index aaf9b973cda..df408e5fb60 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -1,3 +1,11 @@ +- users = capture_haml do + - if search_tabs?(:members) + %li{ class: active_when(@scope == 'users') } + = link_to search_filter_path(scope: 'users') do + Users + %span.badge.badge-pill + = limited_count(@search_results.limited_users_count) + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') @@ -45,6 +53,7 @@ = _("Commits") %span.badge.badge-pill = @search_results.commits_count + = users - elsif @show_snippets %li{ class: active_when(@scope == 'snippet_blobs') } @@ -78,3 +87,4 @@ = _("Milestones") %span.badge.badge-pill = limited_count(@search_results.limited_milestones_count) + = users diff --git a/app/views/search/results/_user.html.haml b/app/views/search/results/_user.html.haml new file mode 100644 index 00000000000..8060a1577e4 --- /dev/null +++ b/app/views/search/results/_user.html.haml @@ -0,0 +1,10 @@ +%ul.content-list + %li + .avatar-cell.d-none.d-sm-block + = user_avatar(user: user, user_name: user.name, css_class: 'd-none d-sm-inline avatar s40') + .user-info + = link_to user_path(user), class: 'd-none d-sm-inline' do + .item-title + = user.name + = user_status(user) + .cgray= user.to_reference diff --git a/changelogs/unreleased/feature-users-search-results.yml b/changelogs/unreleased/feature-users-search-results.yml new file mode 100644 index 00000000000..151d08bce12 --- /dev/null +++ b/changelogs/unreleased/feature-users-search-results.yml @@ -0,0 +1,5 @@ +--- +title: Add users search results to global search +merge_request: 21197 +author: Alexis Reigel +type: added diff --git a/doc/api/search.md b/doc/api/search.md index 330047e323b..6ee3d32d8bc 100644 --- a/doc/api/search.md +++ b/doc/api/search.md @@ -17,7 +17,7 @@ GET /search | `scope` | string | yes | The scope to search in | | `search` | string | yes | The search query | -Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, snippet_blobs. +Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, snippet_blobs, users. The response depends on the requested scope. @@ -281,6 +281,27 @@ Example response: ] ``` +### Scope: users + +```bash +curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/search?scope=users&search=doe +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "John Doe1", + "username": "user1", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", + "web_url": "http://localhost/user1" + } +] +``` + ## Group Search API Search within the specified group. @@ -297,7 +318,7 @@ GET /groups/:id/search | `scope` | string | yes | The scope to search in | | `search` | string | yes | The search query | -Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones. +Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, users. The response depends on the requested scope. @@ -499,6 +520,27 @@ Example response: ] ``` +### Scope: users + +```bash +curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/3/search?scope=users&search=doe +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "John Doe1", + "username": "user1", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", + "web_url": "http://localhost/user1" + } +] +``` + ## Project Search API Search within the specified project. @@ -515,7 +557,7 @@ GET /projects/:id/search | `scope` | string | yes | The scope to search in | | `search` | string | yes | The search query | -Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs. +Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs, users. The response depends on the requested scope. @@ -828,4 +870,25 @@ Example response: ] ``` +### Scope: users + +```bash +curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/6/search?scope=users&search=doe +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "John Doe1", + "username": "user1", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", + "web_url": "http://localhost/user1" + } +] +``` + [ce-41763]: https://gitlab.com/gitlab-org/gitlab-ce/issues/41763 diff --git a/lib/api/helpers/search_helpers.rb b/lib/api/helpers/search_helpers.rb index 47fb5a36327..0e052e0e273 100644 --- a/lib/api/helpers/search_helpers.rb +++ b/lib/api/helpers/search_helpers.rb @@ -5,17 +5,17 @@ module API module SearchHelpers def self.global_search_scopes # This is a separate method so that EE can redefine it. - %w(projects issues merge_requests milestones snippet_titles snippet_blobs) + %w(projects issues merge_requests milestones snippet_titles snippet_blobs users) end def self.group_search_scopes # This is a separate method so that EE can redefine it. - %w(projects issues merge_requests milestones) + %w(projects issues merge_requests milestones users) end def self.project_search_scopes # This is a separate method so that EE can redefine it. - %w(issues merge_requests milestones notes wiki_blobs commits blobs) + %w(issues merge_requests milestones notes wiki_blobs commits blobs users) end end end diff --git a/lib/api/search.rb b/lib/api/search.rb index f65e810bf90..60095300ea1 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -17,7 +17,8 @@ module API blobs: Entities::Blob, wiki_blobs: Entities::Blob, snippet_titles: Entities::Snippet, - snippet_blobs: Entities::Snippet + snippet_blobs: Entities::Snippet, + users: Entities::UserBasic }.freeze def search(additional_params = {}) @@ -51,6 +52,12 @@ module API # Defining this method here as a noop allows us to easily extend it in # EE, without having to modify this file directly. end + + def check_users_search_allowed! + if params[:scope].to_sym == :users && Feature.disabled?(:users_search, default_enabled: true) + render_api_error!({ error: _("Scope not supported with disabled 'users_search' feature!") }, 400) + end + end end resource :search do @@ -67,6 +74,7 @@ module API end get do verify_search_scope! + check_users_search_allowed! present search, with: entity end @@ -87,6 +95,7 @@ module API end get ':id/(-/)search' do verify_search_scope! + check_users_search_allowed! present search(group_id: user_group.id), with: entity end @@ -106,6 +115,8 @@ module API use :pagination end get ':id/(-/)search' do + check_users_search_allowed! + present search(project_id: user_project.id), with: entity end end diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb new file mode 100644 index 00000000000..7255293b194 --- /dev/null +++ b/lib/gitlab/group_search_results.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + class GroupSearchResults < SearchResults + def initialize(current_user, limit_projects, group, query, default_project_filter: false, per_page: 20) + super(current_user, limit_projects, query, default_project_filter: default_project_filter, per_page: per_page) + + @group = group + end + + # rubocop:disable CodeReuse/ActiveRecord + def users + # 1: get all groups the current user has access to + groups = GroupsFinder.new(current_user).execute.joins(:users) + + # 2: Get the group's whole hierarchy + group_users = @group.direct_and_indirect_users + + # 3: get all users the current user has access to (-> + # `SearchResults#users`), which also applies the query. + users = super + + # 4: filter for users that belong to the previously selected groups + users + .where(id: group_users.select('id')) + .where(id: groups.select('members.user_id')) + end + # rubocop:enable CodeReuse/ActiveRecord + end +end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index a68f8801c2a..58f06b6708c 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -22,11 +22,17 @@ module Gitlab paginated_blobs(wiki_blobs, page) when 'commits' Kaminari.paginate_array(commits).page(page).per(per_page) + when 'users' + users.page(page).per(per_page) else super(scope, page, false) end end + def users + super.where(id: @project.team.members) # rubocop:disable CodeReuse/ActiveRecord + end + def blobs_count @blobs_count ||= blobs.count end diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 491148ec1a6..8988b9ad7be 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -32,6 +32,8 @@ module Gitlab merge_requests.page(page).per(per_page) when 'milestones' milestones.page(page).per(per_page) + when 'users' + users.page(page).per(per_page) else Kaminari.paginate_array([]).page(page).per(per_page) end @@ -71,6 +73,12 @@ module Gitlab end # rubocop: enable CodeReuse/ActiveRecord + # rubocop:disable CodeReuse/ActiveRecord + def limited_users_count + @limited_users_count ||= users.limit(count_limit).count + end + # rubocop:enable CodeReuse/ActiveRecord + def single_commit_result? false end @@ -79,6 +87,12 @@ module Gitlab 1001 end + def users + return User.none unless Ability.allowed?(current_user, :read_users_list) + + UsersFinder.new(current_user, search: query).execute + end + private def projects diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e17aab66243..c0c7041500d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6719,6 +6719,9 @@ msgstr "" msgid "Scope" msgstr "" +msgid "Scope not supported with disabled 'users_search' feature!" +msgstr "" + msgid "Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right." msgstr "" diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb new file mode 100644 index 00000000000..3725143291d --- /dev/null +++ b/spec/features/search/user_searches_for_users_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe 'User searches for users' do + context 'when on the dashboard' do + it 'finds the user' do + create(:user, username: 'gob_bluth', name: 'Gob Bluth') + + sign_in(create(:user)) + + visit dashboard_projects_path + + fill_in 'search', with: 'gob' + click_button 'Go' + + expect(page).to have_content('Users 1') + + click_on('Users 1') + + expect(page).to have_content('Gob Bluth') + expect(page).to have_content('@gob_bluth') + end + end + + context 'when on the project page' do + it 'finds the user belonging to the project' do + project = create(:project) + + user1 = create(:user, username: 'gob_bluth', name: 'Gob Bluth') + create(:project_member, :developer, user: user1, project: project) + + user2 = create(:user, username: 'michael_bluth', name: 'Michael Bluth') + create(:project_member, :developer, user: user2, project: project) + + create(:user, username: 'gob_2018', name: 'George Oscar Bluth') + + sign_in(user1) + + visit projects_path(project) + + fill_in 'search', with: 'gob' + click_button 'Go' + + expect(page).to have_content('Gob Bluth') + expect(page).to have_content('@gob_bluth') + + expect(page).not_to have_content('Michael Bluth') + expect(page).not_to have_content('@michael_bluth') + + expect(page).not_to have_content('George Oscar Bluth') + expect(page).not_to have_content('@gob_2018') + end + end + + context 'when on the group page' do + it 'finds the user belonging to the group' do + group = create(:group) + + user1 = create(:user, username: 'gob_bluth', name: 'Gob Bluth') + create(:group_member, :developer, user: user1, group: group) + + user2 = create(:user, username: 'michael_bluth', name: 'Michael Bluth') + create(:group_member, :developer, user: user2, group: group) + + create(:user, username: 'gob_2018', name: 'George Oscar Bluth') + + sign_in(user1) + + visit group_path(group) + + fill_in 'search', with: 'gob' + click_button 'Go' + + expect(page).to have_content('Gob Bluth') + expect(page).to have_content('@gob_bluth') + + expect(page).not_to have_content('Michael Bluth') + expect(page).not_to have_content('@michael_bluth') + + expect(page).not_to have_content('George Oscar Bluth') + expect(page).not_to have_content('@gob_2018') + end + end +end diff --git a/spec/lib/gitlab/group_search_results_spec.rb b/spec/lib/gitlab/group_search_results_spec.rb new file mode 100644 index 00000000000..2734fcef0a0 --- /dev/null +++ b/spec/lib/gitlab/group_search_results_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Gitlab::GroupSearchResults do + let(:user) { create(:user) } + + describe 'user search' do + let(:group) { create(:group) } + + it 'returns the users belonging to the group matching the search query' do + user1 = create(:user, username: 'gob_bluth') + create(:group_member, :developer, user: user1, group: group) + + user2 = create(:user, username: 'michael_bluth') + create(:group_member, :developer, user: user2, group: group) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'returns the user belonging to the subgroup matching the search query', :nested_groups do + user1 = create(:user, username: 'gob_bluth') + subgroup = create(:group, parent: group) + create(:group_member, :developer, user: user1, group: subgroup) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'returns the user belonging to the parent group matching the search query', :nested_groups do + user1 = create(:user, username: 'gob_bluth') + parent_group = create(:group, children: [group]) + create(:group_member, :developer, user: user1, group: parent_group) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'does not return the user belonging to the private subgroup', :nested_groups do + user1 = create(:user, username: 'gob_bluth') + subgroup = create(:group, :private, parent: group) + create(:group_member, :developer, user: user1, group: subgroup) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [] + end + + it 'does not return the user belonging to an unrelated group' do + user = create(:user, username: 'gob_bluth') + unrelated_group = create(:group) + create(:group_member, :developer, user: user, group: unrelated_group) + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [] + end + end +end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 6831274d37c..4a41d5cf51e 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -412,4 +412,36 @@ describe Gitlab::ProjectSearchResults do end end end + + describe 'user search' do + it 'returns the user belonging to the project matching the search query' do + project = create(:project) + + user1 = create(:user, username: 'gob_bluth') + create(:project_member, :developer, user: user1, project: project) + + user2 = create(:user, username: 'michael_bluth') + create(:project_member, :developer, user: user2, project: project) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, project, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'returns the user belonging to the group matching the search query' do + group = create(:group) + project = create(:project, namespace: group) + + user1 = create(:user, username: 'gob_bluth') + create(:group_member, :developer, user: user1, group: group) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, project, 'gob').objects('users') + + expect(result).to eq [user1] + end + end end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 87288baedb0..4b57eecff93 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -121,6 +121,22 @@ describe Gitlab::SearchResults do results.objects('issues') end end + + describe '#users' do + it 'does not call the UsersFinder when the current_user is not allowed to read users list' do + allow(Ability).to receive(:allowed?).and_return(false) + + expect(UsersFinder).not_to receive(:new).with(user, search: 'foo').and_call_original + + results.objects('users') + end + + it 'calls the UsersFinder' do + expect(UsersFinder).to receive(:new).with(user, search: 'foo').and_call_original + + results.objects('users') + end + end end it 'does not list issues on private projects' do diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index c48ca832c85..49672591b3b 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -77,6 +77,28 @@ describe API::Search do it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' end + context 'for users scope' do + before do + create(:user, name: 'billy') + + get api('/search', user), params: { scope: 'users', search: 'billy' } + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + + context 'when users search feature is disabled' do + before do + allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true) + + get api('/search', user), params: { scope: 'users', search: 'billy' } + end + + it 'returns 400 error' do + expect(response).to have_gitlab_http_status(400) + end + end + end + context 'for snippet_titles scope' do before do create(:snippet, :public, title: 'awesome snippet', content: 'snippet content') @@ -192,6 +214,40 @@ describe API::Search do it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' end + + context 'for users scope' do + before do + user = create(:user, name: 'billy') + create(:group_member, :developer, user: user, group: group) + + get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' } + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + + context 'when users search feature is disabled' do + before do + allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true) + + get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' } + end + + it 'returns 400 error' do + expect(response).to have_gitlab_http_status(400) + end + end + end + + context 'for users scope with group path as id' do + before do + user1 = create(:user, name: 'billy') + create(:group_member, :developer, user: user1, group: group) + + get api("/groups/#{CGI.escape(group.full_path)}/search", user), params: { scope: 'users', search: 'billy' } + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + end end end @@ -269,6 +325,29 @@ describe API::Search do it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' end + context 'for users scope' do + before do + user1 = create(:user, name: 'billy') + create(:project_member, :developer, user: user1, project: project) + + get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' } + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + + context 'when users search feature is disabled' do + before do + allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true) + + get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' } + end + + it 'returns 400 error' do + expect(response).to have_gitlab_http_status(400) + end + end + end + context 'for notes scope' do before do create(:note_on_merge_request, project: project, note: 'awesome note') |