diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index 71e92d38d2b5b9301760cf94c1fe576b0c2dbff6..2da60682d417562bdc5d98ba5b18ae9fbb8c85ad 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -6,7 +6,7 @@ class GlobalService include Gitlab::Utils::StrongMemoize DEFAULT_SCOPE = 'projects' - ALLOWED_SCOPES = %w[projects issues merge_requests milestones users].freeze + ALLOWED_SCOPES = %w[projects issues merge_requests milestones work_items users].freeze attr_accessor :current_user, :params diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb index 40f7850e2b022aa4e3df20cb185ceb9213f3927a..9d3ebe8d7d046b4f512256a9395e2a186a84eb16 100644 --- a/app/services/search/project_service.rb +++ b/app/services/search/project_service.rb @@ -5,7 +5,7 @@ class ProjectService include Search::Filter include Gitlab::Utils::StrongMemoize - ALLOWED_SCOPES = %w[blobs issues merge_requests wiki_blobs commits notes milestones users].freeze + ALLOWED_SCOPES = %w[blobs commits issues merge_requests milestones notes wiki_blobs work_items users].freeze attr_accessor :project, :current_user, :params diff --git a/ee/app/finders/ee/work_items/glql/work_items_finder.rb b/ee/app/finders/ee/work_items/glql/work_items_finder.rb index 9c4013317c83fd711f4c36ef70e5a1c12dc8f551..2e00e3f7135b6e1cf9d861cfcfc8019b0fd92d99 100644 --- a/ee/app/finders/ee/work_items/glql/work_items_finder.rb +++ b/ee/app/finders/ee/work_items/glql/work_items_finder.rb @@ -56,7 +56,7 @@ module WorkItemsFinder attr_accessor :resource_parent def execute - result = search_service.search_results.objects('issues') + result = search_service.search_results.objects('work_items') ::WorkItem.glql_from_es_results(result) end diff --git a/ee/lib/gitlab/elastic/group_search_results.rb b/ee/lib/gitlab/elastic/group_search_results.rb index 75ed427030d478b59a03571033ca25dc553237f8..aaa3bb4902811b93ff2fed764c1ca7894d946218 100644 --- a/ee/lib/gitlab/elastic/group_search_results.rb +++ b/ee/lib/gitlab/elastic/group_search_results.rb @@ -40,7 +40,7 @@ def scope_options(scope) super.merge(root_ancestor_ids: [group.root_ancestor.id]) when :users super.except(:group_ids) # User uses group_id for namespace_query - when :work_items + when :work_items, :issues options = super.merge(root_ancestor_ids: [group.root_ancestor.id]) if !glql_query?(source) && Feature.enabled?(:search_work_item_queries_notes, current_user) diff --git a/ee/lib/gitlab/elastic/project_search_results.rb b/ee/lib/gitlab/elastic/project_search_results.rb index 44a3f1558f8afff2540dd2ec4506e38604cc971b..17c573d7bd77123773824feaae49ad8a92f02f0f 100644 --- a/ee/lib/gitlab/elastic/project_search_results.rb +++ b/ee/lib/gitlab/elastic/project_search_results.rb @@ -108,7 +108,7 @@ def scope_options(scope) base_options.merge(filters.slice(:language, :num_context_lines)) when :users super.merge(project_id: project.id) - when :work_items + when :work_items, :issues options = super.merge(filters.slice(:hybrid_similarity, :hybrid_boost)) options[:root_ancestor_ids] = [project.root_ancestor.id] if !glql_query?(source) && Feature.enabled?(:search_work_item_queries_notes, current_user) diff --git a/ee/lib/gitlab/elastic/search_results.rb b/ee/lib/gitlab/elastic/search_results.rb index b9238fce5e8303970c62e729e58772e378af9fa7..fae5c78c08cb14cdc9b01fc8b2d289d07ed6702f 100644 --- a/ee/lib/gitlab/elastic/search_results.rb +++ b/ee/lib/gitlab/elastic/search_results.rb @@ -118,15 +118,23 @@ def initialize(current_user, query, limit_project_ids = nil, **opts) end def failed?(scope) - return false unless scope == 'issues' - - issues.failed? + case scope + when 'issues' + issues.failed? + when 'work_items' + work_items.failed? + else + false + end end def error(scope) - return unless scope == 'issues' - - issues.error + case scope + when 'issues' + issues.error + when 'work_items' + work_items.error + end end def objects(scope, page: 1, per_page: DEFAULT_PER_PAGE, preload_method: nil) @@ -153,6 +161,8 @@ def objects(scope, page: 1, per_page: DEFAULT_PER_PAGE, preload_method: nil) commits(page: page, per_page: per_page, preload_method: preload_method) when 'users' users(page: page, per_page: per_page, preload_method: preload_method) + when 'work_items' + work_items(page: page, per_page: per_page, preload_method: preload_method).paginated_array else Kaminari.paginate_array([]) end @@ -166,6 +176,8 @@ def highlight_map(scope) create_map(projects) when 'issues' issues.highlight_map + when 'work_items' + work_items.highlight_map when 'merge_requests' create_map(merge_requests) when 'milestones' @@ -199,9 +211,19 @@ def formatted_count(scope) elastic_search_limited_counter_with_delimiter(milestones_count) when 'users' elastic_search_limited_counter_with_delimiter(users_count) + when 'work_items' + elastic_search_limited_counter_with_delimiter(work_items_count) end end + def work_items_count + @work_items_count ||= if strong_memoized?(:work_items) + work_items.total_count + else + work_items(count_only: true).total_count + end + end + def projects_count @projects_count ||= if strong_memoized?(:projects) projects.total_count @@ -292,6 +314,7 @@ def milestones_count alias_method :limited_issues_count, :issues_count alias_method :limited_merge_requests_count, :merge_requests_count alias_method :limited_milestones_count, :milestones_count + alias_method :limited_work_items_count, :work_items_count def aggregations(scope) case scope @@ -299,6 +322,8 @@ def aggregations(scope) blob_aggregations when 'issues' issue_aggregations + when 'work_items' + work_item_aggregations when 'merge_requests' merge_request_aggregations else @@ -345,20 +370,12 @@ def scope_options(scope) case scope when :projects, :notes, :commits base_options.merge(filters.slice(:include_archived)) - when :work_items # issues - options = work_item_scope_options - if !glql_query?(source) && - !::Gitlab::Saas.feature_available?(:advanced_search) && - Feature.enabled?(:search_work_item_queries_notes, current_user) - options[:related_ids] = related_ids_for_notes(Issue.name) - end - - options + when :work_items + base_work_item_options(WorkItem) when :merge_requests base_options.merge(merge_request_scope_options) when :issues - base_options.merge( - filters.slice(:order_by, :sort, :confidential, :state, :label_name, :include_archived), klass: Issue) + issues_scope_options when :milestones # Must pass 'issues' and 'merge_requests' to check # if any of the features is available for projects in ApplicationClassProxy#project_ids_query @@ -366,11 +383,7 @@ def scope_options(scope) # from projects with milestones disabled. base_options.merge({ features: [:issues, :merge_requests] }, filters.slice(:include_archived)) when :epics - work_item_scope_options.merge( - not_work_item_type_ids: nil, - klass: WorkItem, - work_item_type_ids: [::WorkItems::Type.find_by_name(::WorkItems::Type::TYPE_NAMES[:epic]).id] - ).except(:fields) + epic_scope_options when :users user_scope_options when :blobs @@ -382,22 +395,41 @@ def scope_options(scope) end end - def work_item_scope_options - work_item_scope_options = base_options.merge( + def base_work_item_options(klass) + options = base_options.merge( { - klass: Issue, # For rendering the UI - index_name: ::Search::Elastic::References::WorkItem.index, - not_work_item_type_ids: [::WorkItems::Type.find_by_name(::WorkItems::Type::TYPE_NAMES[:epic]).id] - }, - filters.slice(*::Search::Elastic::References::WorkItem::PERMITTED_FILTER_KEYS) + **filters.slice(*::Search::Elastic::References::WorkItem::PERMITTED_FILTER_KEYS), + klass: klass, + index_name: ::Search::Elastic::References::WorkItem.index + } ) if filters[:type].present? work_item_type_id = ::WorkItems::Type.find_by_name(::WorkItems::Type::TYPE_NAMES[filters[:type].to_sym])&.id - work_item_scope_options[:work_item_type_ids] = [work_item_type_id] unless work_item_type_id.nil? + options[:work_item_type_ids] = [work_item_type_id] unless work_item_type_id.nil? end - work_item_scope_options + options + end + + def epic_scope_options + base_work_item_options(WorkItem).tap do |options| + # NOTE: search only for epic type + options[:work_item_type_ids] = [::WorkItems::Type.find_by_name(::WorkItems::Type::TYPE_NAMES[:epic]).id] + end.except(:fields) + end + + def issues_scope_options + base_work_item_options(Issue).tap do |options| + # NOTE: exclude epic type from results + options[:not_work_item_type_ids] = [::WorkItems::Type.find_by_name(::WorkItems::Type::TYPE_NAMES[:epic]).id] + options[:related_ids] = related_ids_for_notes(Issue.name) if search_for_notes? + end + end + + def search_for_notes? + !glql_query?(source) && !::Gitlab::Saas.feature_available?(:advanced_search) && Feature.enabled?( + :search_work_item_queries_notes, current_user) end def user_scope_options @@ -428,6 +460,19 @@ def projects(count_only: false) def issues(page: 1, per_page: DEFAULT_PER_PAGE, count_only: false, preload_method: nil) strong_memoize(memoize_key('issues', count_only: count_only)) do + options = scope_options(:issues).merge(count_only: count_only, per_page: per_page, + page: page, preload_method: preload_method) + + search_query = ::Search::Elastic::WorkItemQueryBuilder.build(query: query, options: options) + + ::Gitlab::Search::Client.execute_search(query: search_query, options: options) do |response| + ::Search::Elastic::ResponseMapper.new(response, options) + end + end + end + + def work_items(page: 1, per_page: DEFAULT_PER_PAGE, count_only: false, preload_method: nil) + strong_memoize(memoize_key('work_items', count_only: count_only)) do options = scope_options(:work_items).merge(count_only: count_only, per_page: per_page, page: page, preload_method: preload_method) @@ -546,7 +591,7 @@ def blob_aggregations end strong_memoize_attr :blob_aggregations - def issue_aggregations + def work_item_aggregations options = scope_options(:work_items).merge(aggregation: true) search_query = ::Search::Elastic::WorkItemQueryBuilder.build(query: query, options: options) @@ -555,6 +600,17 @@ def issue_aggregations end ::Gitlab::Search::AggregationParser.call(results.aggregations) end + strong_memoize_attr :work_item_aggregations + + def issue_aggregations + options = scope_options(:issues).merge(aggregation: true) + search_query = ::Search::Elastic::WorkItemQueryBuilder.build(query: query, options: options) + + results = ::Gitlab::Search::Client.execute_search(query: search_query, options: options) do |response| + ::Search::Elastic::ResponseMapper.new(response, options) + end + ::Gitlab::Search::AggregationParser.call(results.aggregations) + end strong_memoize_attr :issue_aggregations def merge_request_aggregations diff --git a/ee/spec/lib/gitlab/elastic/group_search_results_spec.rb b/ee/spec/lib/gitlab/elastic/group_search_results_spec.rb index 45d04d4b43ad7d4407de42db7ddc27111f6d6cb1..7c3daf4e3dd7e2f8303375d83548aef457f4375f 100644 --- a/ee/spec/lib/gitlab/elastic/group_search_results_spec.rb +++ b/ee/spec/lib/gitlab/elastic/group_search_results_spec.rb @@ -219,6 +219,7 @@ end end + # epics scope context 'for group level work items' do let(:query) { 'foo' } let(:scope) { 'epics' } @@ -344,6 +345,40 @@ end end + context 'for project level work items' do + let_it_be(:project) { create(:project, :public, group: group, developers: user) } + let_it_be(:closed_result) { create(:work_item, :closed, project: project, title: 'foo closed') } + let_it_be(:opened_result) { create(:work_item, :opened, project: project, title: 'foo opened') } + let_it_be(:confidential_result) { create(:work_item, :confidential, project: project, title: 'foo confidential') } + + let(:query) { 'foo' } + let(:scope) { 'work_items' } + + before do + ::Elastic::ProcessInitialBookkeepingService.backfill_projects!(project) + ensure_elasticsearch_index! + end + + context 'when advanced search query syntax is used' do + let(:query) { 'foo -banner' } + + include_examples 'search results filtered by state' + include_examples 'search results filtered by confidential' + include_examples 'search results filtered by labels', document_type: :work_item + + it_behaves_like 'namespace ancestry_filter for aggregations' do + let(:query_name) { 'filters:permissions:group:private_access:ancestry_filter:descendants' } + end + end + + include_examples 'search results filtered by state' + include_examples 'search results filtered by confidential' + include_examples 'search results filtered by labels', document_type: :work_item + it_behaves_like 'namespace ancestry_filter for aggregations' do + let(:query_name) { 'filters:permissions:group:private_access:ancestry_filter:descendants' } + end + end + context 'for users' do let(:query) { 'john' } let(:scope) { 'users' } @@ -434,8 +469,8 @@ end describe 'query performance' do - allowed_scopes = %w[projects notes blobs wiki_blobs commits issues merge_requests epics milestones users] - scopes_with_notes_query = %w[issues] + allowed_scopes = %w[projects notes blobs wiki_blobs commits issues work_items merge_requests epics milestones users] + scopes_with_notes_query = %w[issues work_items] include_examples 'calls Elasticsearch the expected number of times', scopes: (allowed_scopes - scopes_with_notes_query), scopes_with_multiple: scopes_with_notes_query @@ -460,6 +495,7 @@ %W[wiki_blobs #{Wiki.index_name}], %W[commits #{Elastic::Latest::CommitConfig.index_name}], %W[issues #{::Search::Elastic::References::WorkItem.index}], + %W[work_items #{::Search::Elastic::References::WorkItem.index}], %W[merge_requests #{MergeRequest.index_name}], %W[epics #{::Search::Elastic::References::WorkItem.index}], %W[milestones #{Milestone.index_name}], @@ -481,6 +517,12 @@ end end + context ':issues' do + it 'has root_ancestor_ids' do + expect(subject.scope_options(:issues)).to include :root_ancestor_ids + end + end + context ':epics' do it 'has root_ancestor_ids' do expect(subject.scope_options(:epics)).to include :root_ancestor_ids diff --git a/ee/spec/lib/gitlab/elastic/project_search_results_spec.rb b/ee/spec/lib/gitlab/elastic/project_search_results_spec.rb index ee80dda30f4c0d98c77c1643abc0c82b420cf6be..12de188226cbbcdda5cec3fcce5ea9674f40118b 100644 --- a/ee/spec/lib/gitlab/elastic/project_search_results_spec.rb +++ b/ee/spec/lib/gitlab/elastic/project_search_results_spec.rb @@ -34,7 +34,7 @@ it { expect(results.query).to eq('hello world') } end - describe "search", :sidekiq_inline do + describe 'search', :sidekiq_inline do let_it_be(:project) { create(:project, :public, :repository, :wiki_repo) } let_it_be(:private_project) { create(:project, :repository, :wiki_repo) } @@ -106,6 +106,51 @@ include_examples 'search results filtered by state' include_examples 'search results filtered by confidential' include_examples 'search results filtered by labels' + + describe 'confidential issues' do + include_examples 'access restricted confidential objects' do + before do + ensure_elasticsearch_index! + end + end + end + end + + context 'for project level work items' do + let(:scope) { 'work_items' } + + let_it_be(:closed_result) { create(:work_item, :closed, project: project, title: 'foo closed') } + let_it_be(:opened_result) { create(:work_item, :opened, project: project, title: 'foo opened') } + let_it_be(:confidential_result) do + create(:work_item, :confidential, project: project, title: 'foo confidential') + end + + before do + ::Elastic::ProcessInitialBookkeepingService.track!(opened_result) + ::Elastic::ProcessInitialBookkeepingService.track!(closed_result) + ::Elastic::ProcessInitialBookkeepingService.track!(confidential_result) + ensure_elasticsearch_index! + end + + context 'when advanced search query syntax is used' do + let(:query) { 'foo -banner' } + + include_examples 'search results filtered by state' + include_examples 'search results filtered by confidential' + include_examples 'search results filtered by labels', document_type: :work_item + end + + include_examples 'search results filtered by state' + include_examples 'search results filtered by confidential' + include_examples 'search results filtered by labels', document_type: :work_item + + describe 'confidential work items' do + include_examples 'access restricted confidential objects', document_type: :work_item do + before do + ensure_elasticsearch_index! + end + end + end end context 'for merge_requests' do @@ -129,14 +174,6 @@ end end - describe 'confidential issues', :sidekiq_might_not_need_inline do - include_examples 'access restricted confidential issues' do - before do - ensure_elasticsearch_index! - end - end - end - describe 'users' do let(:query) { 'john' } let(:scope) { 'users' } @@ -230,8 +267,8 @@ create(:wiki_page, wiki: project.wiki) end - allowed_scopes = %w[notes blobs wiki_blobs commits merge_requests milestones users] - scopes_with_notes_query = %w[issues] + allowed_scopes = %w[notes blobs wiki_blobs commits merge_requests milestones users issues work_items] + scopes_with_notes_query = %w[issues work_items] include_examples 'calls Elasticsearch the expected number of times', scopes: (allowed_scopes - scopes_with_notes_query), scopes_with_multiple: scopes_with_notes_query @@ -251,6 +288,7 @@ %W[wiki_blobs #{Wiki.index_name}], %W[commits #{Elastic::Latest::CommitConfig.index_name}], %W[issues #{::Search::Elastic::References::WorkItem.index}], + %W[work_items #{::Search::Elastic::References::WorkItem.index}], %W[merge_requests #{MergeRequest.index_name}], %W[milestones #{Milestone.index_name}], %W[users #{User.index_name}] @@ -269,6 +307,7 @@ 'milestones' | nil | false 'notes' | nil | false 'issues' | 'labels' | false + 'work_items' | 'labels' | false 'merge_requests' | 'labels' | false 'wiki_blobs' | nil | false 'commits' | nil | false diff --git a/ee/spec/lib/gitlab/elastic/search_results_query_performance_spec.rb b/ee/spec/lib/gitlab/elastic/search_results_query_performance_spec.rb index c8dd4a426bd28901cca9ac1848e270532dbc7d88..26b2789b6f5fe358fe67bdfdf66b04ebfd3a9cd9 100644 --- a/ee/spec/lib/gitlab/elastic/search_results_query_performance_spec.rb +++ b/ee/spec/lib/gitlab/elastic/search_results_query_performance_spec.rb @@ -15,8 +15,9 @@ describe 'query performance' do let(:results) { described_class.new(user, query, limit_project_ids) } - allowed_scopes = %w[projects notes blobs wiki_blobs issues commits merge_requests epics milestones notes users] - scopes_with_notes_query = %w[issues] + allowed_scopes = %w[projects notes blobs wiki_blobs issues work_items commits merge_requests epics milestones notes + users] + scopes_with_notes_query = %w[issues work_items] include_examples 'calls Elasticsearch the expected number of times', scopes: (allowed_scopes - scopes_with_notes_query), scopes_with_multiple: scopes_with_notes_query @@ -37,6 +38,7 @@ %W[wiki_blobs #{Wiki.index_name}], %W[commits #{Elastic::Latest::CommitConfig.index_name}], %W[issues #{::Search::Elastic::References::WorkItem.index}], + %W[work_items #{::Search::Elastic::References::WorkItem.index}], %W[merge_requests #{MergeRequest.index_name}], %W[epics #{::Search::Elastic::References::WorkItem.index}], %W[milestones #{Milestone.index_name}], diff --git a/ee/spec/lib/gitlab/elastic/search_results_spec.rb b/ee/spec/lib/gitlab/elastic/search_results_spec.rb index 49f044ce41aca7cf8eb169265c43bdf2c85fca74..12b32f52924354d4ecc759ad6cc53c892092adbe 100644 --- a/ee/spec/lib/gitlab/elastic/search_results_spec.rb +++ b/ee/spec/lib/gitlab/elastic/search_results_spec.rb @@ -30,6 +30,8 @@ 'notes' | :notes | ref(:proxy_response) | ref(:map) 'issues' | :issues | ref(:es_client_response) | ref(:map) 'issues' | :issues | ref(:es_empty_response) | {} + 'work_items' | :work_items | ref(:es_client_response) | ref(:map) + 'work_items' | :work_items | ref(:es_empty_response) | {} 'merge_requests' | :merge_requests | ref(:proxy_response) | ref(:map) 'blobs' | nil | nil | {} 'wiki_blobs' | nil | nil | {} @@ -61,6 +63,7 @@ 'wiki_blobs' | :wiki_blobs_count | 1111 | '1,111' 'commits' | :commits_count | 9999 | '9,999' 'issues' | :issues_count | 10000 | '10,000+' + 'work_items' | :work_items_count | 50000 | '10,000+' 'merge_requests' | :merge_requests_count | 20000 | '10,000+' 'milestones' | :milestones_count | nil | '0' 'epics' | :epics_count | 200 | '200' @@ -89,6 +92,7 @@ 'milestones' | nil | false 'notes' | nil | false 'issues' | 'labels' | false + 'work_items' | 'labels' | false 'merge_requests' | 'labels' | false 'wiki_blobs' | nil | false 'commits' | nil | false @@ -278,15 +282,17 @@ let(:results) { described_class.new(user, query, limit_project_ids) } let(:response_mapper) { instance_double(::Search::Elastic::ResponseMapper, failed?: true) } - before do - allow(results).to receive(:issues).and_return(response_mapper) + where(:scope) do + [:issues, :work_items] end - context 'for issues scope' do - let(:scope) { 'issues' } + with_them do + before do + allow(results).to receive(scope).and_return(response_mapper) + end it 'returns failed from the response mapper' do - expect(results.failed?(scope)).to be true + expect(results.failed?(scope.to_s)).to be true end end @@ -303,15 +309,17 @@ let(:results) { described_class.new(user, query, limit_project_ids) } let(:response_mapper) { instance_double(::Search::Elastic::ResponseMapper, error: 'An error occurred') } - before do - allow(results).to receive(:issues).and_return(response_mapper) + where(:scope) do + [:issues, :work_items] end - context 'for issues scope' do - let(:scope) { 'issues' } + with_them do + before do + allow(results).to receive(scope).and_return(response_mapper) + end it 'returns the error from the response mapper' do - expect(results.error(scope)).to eq 'An error occurred' + expect(results.error(scope.to_s)).to eq 'An error occurred' end end diff --git a/ee/spec/lib/gitlab/elastic/search_results_visibility_levels_spec.rb b/ee/spec/lib/gitlab/elastic/search_results_visibility_levels_spec.rb index 0d06da4e245d16d63e7318dd697e61591a8e319c..eb8f9848f9199c9111da4c0e68eff1a6767b1c8b 100644 --- a/ee/spec/lib/gitlab/elastic/search_results_visibility_levels_spec.rb +++ b/ee/spec/lib/gitlab/elastic/search_results_visibility_levels_spec.rb @@ -100,6 +100,63 @@ it_behaves_like 'issues respect visibility' end + describe 'project level work items' do + it 'finds right set of work_items' do + work_item_1 = create :work_item, project: internal_project, title: "Internal project" + create :work_item, project: private_project1, title: "Private project" + work_item_3 = create :work_item, project: private_project2, title: "Private project where I'm a member" + work_item_4 = create :work_item, project: public_project, title: "Public project" + + ensure_elasticsearch_index! + + # Authenticated search + results = described_class.new(user, 'project', limit_project_ids) + work_items = results.objects('work_items') + + expect(work_items).to include work_item_1 + expect(work_items).to include work_item_3 + expect(work_items).to include work_item_4 + expect(results.work_items_count).to eq 3 + + # Unauthenticated search + results = described_class.new(nil, 'project', []) + work_items = results.objects('work_items') + + expect(work_items).to include work_item_4 + expect(results.work_items_count).to eq 1 + end + + context 'when different work_item descriptions', :aggregate_failures do + let(:examples) do + code_examples.merge( + 'screen' => 'Screenshots or screen recordings', + 'problem' => 'Problem to solve' + ) + end + + include_context 'with code examples' do + before do + examples.values.uniq.each do |description| + sha = Digest::SHA256.hexdigest(description) + create :work_item, project: private_project2, title: sha, description: description + end + + ensure_elasticsearch_index! + end + + it 'finds all examples' do + examples.each do |search_term, description| + sha = Digest::SHA256.hexdigest(description) + + results = described_class.new(user, search_term, limit_project_ids) + work_items = results.objects('work_items') + expect(work_items.map(&:title)).to include(sha), "failed to find #{search_term}" + end + end + end + end + end + describe 'milestones' do let_it_be_with_reload(:milestone_1) { create(:milestone, project: internal_project, title: "Internal project") } let_it_be_with_reload(:milestone_2) { create(:milestone, project: private_project1, title: "Private project") } diff --git a/ee/spec/lib/gitlab/elastic/search_results_work_items_spec.rb b/ee/spec/lib/gitlab/elastic/search_results_work_items_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7df78f73420f3799760bea5cfa9198358b98b942 --- /dev/null +++ b/ee/spec/lib/gitlab/elastic/search_results_work_items_spec.rb @@ -0,0 +1,444 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Elastic::SearchResults, 'work_items', feature_category: :global_search do + let(:query) { 'hello world' } + let(:scope) { 'work_items' } + let_it_be(:user) { create(:user) } + let_it_be(:project_1) { create(:project, :public, :repository, :wiki_repo, :in_group) } + let_it_be(:project_2) { create(:project, :public, :repository, :wiki_repo) } + let_it_be(:limit_project_ids) { [project_1.id] } + + before do + stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) + end + + describe 'work_items', :elastic_delete_by_query do + let_it_be(:work_item_1) do + create(:work_item, project: project_1, title: 'Hello world, here I am!', + description: '20200623170000, see details in work_item 287661', iid: 1) + end + + let_it_be(:work_item_2) do + create(:work_item, project: project_1, title: 'work_item Two', description: 'Hello world, here I am!', iid: 2) + end + + let_it_be(:work_item_3) { create(:work_item, project: project_2, title: 'work_item Three', iid: 2) } + + before do + Elastic::ProcessInitialBookkeepingService.backfill_projects!(project_1, project_2) + ensure_elasticsearch_index! + end + + it_behaves_like 'a paginated object', 'work_items' + + it 'lists found work items' do + results = described_class.new(user, query, limit_project_ids) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item_1, work_item_2) + expect(results.work_items_count).to eq 2 + end + + it 'returns empty list when work items are not found' do + results = described_class.new(user, 'security', limit_project_ids) + + expect(results.objects('work_items')).to be_empty + expect(results.work_items_count).to eq 0 + end + + it 'lists work items when search by a valid iid' do + results = described_class.new(user, '#2', limit_project_ids) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item_2, work_item_3) + expect(results.work_items_count).to eq 2 + end + + it 'can also find an work_item by iid without the prefixed #' do + results = described_class.new(user, '2', limit_project_ids) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item_3, work_item_2) + expect(results.work_items_count).to eq 2 + end + + it 'finds the work_item with an out of integer range number in its description without exception' do + results = described_class.new(user, '20200623170000', limit_project_ids) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item_1) + expect(results.work_items_count).to eq 1 + end + + it 'returns empty list when search by invalid iid' do + results = described_class.new(user, '#222', limit_project_ids) + + expect(results.objects('work_items')).to be_empty + expect(results.work_items_count).to eq 0 + end + + it_behaves_like 'can search by title for miscellaneous cases', 'work_items' + + it 'executes count only queries' do + results = described_class.new(user, query, limit_project_ids) + expect(results).to receive(:work_items).with(count_only: true).and_call_original + + count = results.work_items_count + + expect(count).to eq(2) + end + + describe 'filtering' do + let_it_be(:project) { create(:project, :public, developers: [user]) } + let_it_be(:closed_result) { create(:work_item, :closed, project: project, title: 'foo closed') } + let_it_be(:opened_result) { create(:work_item, :opened, project: project, title: 'foo opened') } + let_it_be(:confidential_result) { create(:work_item, :confidential, project: project, title: 'foo confidential') } + + let(:results) { described_class.new(user, 'foo', [project.id], filters: filters) } + + before do + ::Elastic::ProcessInitialBookkeepingService.backfill_projects!(project) + ensure_elasticsearch_index! + end + + include_examples 'search results filtered by state' + include_examples 'search results filtered by confidential' + include_examples 'search results filtered by labels', document_type: :work_item + + context 'for work_item_type filter' do + using RSpec::Parameterized::TableSyntax + let_it_be(:requirement) { create(:work_item, :requirement, project: project) } + let_it_be(:task) { create(:work_item, :task, project: project) } + let_it_be(:incident) { create(:work_item, :incident, project: project) } + let_it_be(:work_item) { create(:work_item, project: project) } + + before do + ::Elastic::ProcessBookkeepingService.track!(requirement, task, incident, work_item) + ensure_elasticsearch_index! + end + + where(:type, :expected) do + 'requirement' | [ref(:requirement)] + 'task' | [ref(:task)] + 'incident' | [ref(:incident)] + 'work_item' | [ref(:work_item)] + 'invalid_type' | [ref(:work_item), ref(:incident), ref(:requirement), ref(:task)] + end + + with_them do + it 'returns the expected work_item based on type' do + work_items = described_class.new(user, '*', [project.id], filters: { type: type }).objects('work_items') + expect(work_items).to include(*expected) + end + end + end + end + + describe 'ordering' do + let_it_be(:project) { create(:project, :public) } + + let_it_be(:old_result) do + create(:work_item, project: project, title: 'sorted old', created_at: 1.month.ago) + end + + let_it_be(:new_result) do + create(:work_item, project: project, title: 'sorted recent', created_at: 1.day.ago) + end + + let_it_be(:very_old_result) do + create(:work_item, project: project, title: 'sorted very old', created_at: 1.year.ago) + end + + let_it_be(:old_updated) do + create(:work_item, project: project, title: 'updated old', updated_at: 1.month.ago) + end + + let_it_be(:new_updated) do + create(:work_item, project: project, title: 'updated recent', updated_at: 1.day.ago) + end + + let_it_be(:very_old_updated) do + create(:work_item, project: project, title: 'updated very old', updated_at: 1.year.ago) + end + + let_it_be(:less_popular_result) do + create(:work_item, project: project, title: 'less popular', upvotes_count: 10) + end + + let_it_be(:non_popular_result) do + create(:work_item, project: project, title: 'non popular', upvotes_count: 1) + end + + let_it_be(:popular_result) do + create(:work_item, project: project, title: 'popular', upvotes_count: 100) + end + + before do + ::Elastic::ProcessInitialBookkeepingService.backfill_projects!(project) + ensure_elasticsearch_index! + end + + include_examples 'search results sorted' do + let(:results_created) { described_class.new(user, 'sorted', [project.id], sort: sort) } + let(:results_updated) { described_class.new(user, 'updated', [project.id], sort: sort) } + end + + include_examples 'search results sorted by popularity' do + let(:results_popular) { described_class.new(user, 'popular', [project.id], sort: sort) } + end + end + end + + describe 'confidential work_items', :elastic_delete_by_query do + let_it_be(:project_3) { create(:project, :public) } + let_it_be(:project_4) { create(:project, :public) } + let_it_be(:author) { create(:user) } + let_it_be(:assignee) { create(:user) } + let_it_be(:non_member) { create(:user) } + let_it_be(:member) { create(:user) } + let_it_be(:admin) { create(:admin) } + let_it_be(:work_item) { create(:work_item, project: project_1, title: 'work_item 1', iid: 1) } + let_it_be(:security_issue_1) do + create(:work_item, :confidential, project: project_1, title: 'Security work_item 1', author: author, iid: 2) + end + + let_it_be(:security_issue_2) do + create(:work_item, :confidential, title: 'Security work_item 2', project: project_1, assignees: [assignee], + iid: 3) + end + + let_it_be(:security_issue_3) do + create(:work_item, :confidential, project: project_2, title: 'Security work_item 3', author: author, iid: 1) + end + + let_it_be(:security_issue_4) do + create(:work_item, :confidential, project: project_3, + title: 'Security work_item 4', assignees: [assignee], iid: 1) + end + + let_it_be(:security_issue_5) do + create(:work_item, :confidential, project: project_4, title: 'Security work_item 5', iid: 1) + end + + before do + ::Elastic::ProcessInitialBookkeepingService.backfill_projects!(project_1, project_2, project_3, project_4) + ensure_elasticsearch_index! + end + + context 'when searching by term' do + let(:query) { 'work_item' } + + it 'does not list confidential work_items for anonymous users' do + results = described_class.new(nil, query, []) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item) + expect(results.work_items_count).to eq 1 + end + + it 'does not list confidential work_items for guest users' do + results = described_class.new(member, query, [project_1.id]) + project_1.add_guest(member) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item) + expect(results.work_items_count).to eq 1 + end + + it 'does not list confidential work_items for non project members' do + results = described_class.new(non_member, query, []) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item) + expect(results.work_items_count).to eq 1 + end + + it 'lists confidential work_items for author' do + results = described_class.new(author, query, author.authorized_projects.pluck(:id)) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item, security_issue_1, security_issue_3) + expect(results.work_items_count).to eq 3 + end + + it 'lists confidential work_items for assignee' do + results = described_class.new(assignee, query, assignee.authorized_projects.pluck(:id)) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item, security_issue_2, security_issue_4) + expect(results.work_items_count).to eq 3 + end + + it 'lists confidential work_items from projects for which the user is member with developer access+' do + project_1.add_developer(member) + project_2.add_developer(member) + + results = described_class.new(member, query, member.authorized_projects.pluck(:id)) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item, security_issue_1, security_issue_2, security_issue_3) + expect(results.work_items_count).to eq 4 + end + + context 'for admin users' do + context 'when admin mode enabled', :enable_admin_mode do + it 'lists all work_items' do + results = described_class.new(admin, query, admin.authorized_projects.pluck(:id)) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item, security_issue_1, + security_issue_2, security_issue_3, security_issue_4, security_issue_5) + expect(results.work_items_count).to eq 6 + end + end + + context 'when admin mode disabled' do + it 'does not list confidential work_items' do + results = described_class.new(admin, query, admin.authorized_projects.pluck(:id)) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item) + expect(results.work_items_count).to eq 1 + end + end + end + + context 'for user who is the member of only project which does not have any confidential work_items' do + let_it_be(:other_project) { create(:project) } + let_it_be(:other_project_member_user) { create(:user) } + + it 'does not list any confidential work_items' do + other_project.add_developer(other_project_member_user) + results = described_class.new(other_project_member_user, query, + other_project_member_user.authorized_projects.pluck(:id)) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item) + end + end + end + + context 'when searching by iid' do + let(:query) { '#1' } + + it 'does not list confidential work_items for guests' do + project_1.add_guest(member) + results = described_class.new(nil, query, member.authorized_projects.pluck(:id)) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item) + expect(results.work_items_count).to eq 1 + end + + it 'does not list confidential work_items for non project members' do + results = described_class.new(non_member, query, non_member.authorized_projects.pluck(:id)) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item) + expect(results.work_items_count).to eq 1 + end + + it 'lists confidential work_items for author' do + results = described_class.new(author, query, author.authorized_projects.pluck(:id)) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item, security_issue_3) + expect(results.work_items_count).to eq 2 + end + + it 'lists confidential work_items for assignee' do + results = described_class.new(assignee, query, assignee.authorized_projects.pluck(:id)) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item, security_issue_4) + expect(results.work_items_count).to eq 2 + end + + it 'lists confidential work_items for project members with developer role+' do + project_2.add_developer(member) + project_3.add_developer(member) + + results = described_class.new(member, query, member.authorized_projects.pluck(:id)) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item, security_issue_3, security_issue_4) + expect(results.work_items_count).to eq 3 + end + + context 'for admin users' do + context 'when admin mode enabled', :enable_admin_mode do + it 'lists all work_items' do + results = described_class.new(admin, query, admin.authorized_projects.pluck(:id)) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item, security_issue_3, security_issue_4, security_issue_5) + expect(results.work_items_count).to eq 4 + end + end + + context 'when admin mode disabled' do + it 'does not list confidential work_items' do + results = described_class.new(admin, query, admin.authorized_projects.pluck(:id)) + work_items = results.objects('work_items') + + expect(work_items).to contain_exactly(work_item) + expect(results.work_items_count).to eq 1 + end + end + end + end + end + + describe 'work_items with notes', :elastic_delete_by_query do + let(:query) { 'Goodbye moon' } + let(:source) { nil } + let_it_be(:limit_project_ids) { user.authorized_projects.pluck_primary_key } + let_it_be(:work_item) { create(:work_item, project: project_1, title: 'Hello world, here I am!') } + let_it_be(:note) { create(:note_on_issue, note: 'Goodbye moon', noteable: work_item, project: work_item.project) } + + let(:results) do + described_class.new(user, query, limit_project_ids, public_and_internal_projects: true, source: source) + end + + before do + Elastic::ProcessInitialBookkeepingService.track!(work_item, note) + ensure_elasticsearch_index! + end + + subject(:work_items) { results.objects('work_items') } + + it 'returns the work_item when searching with note text' do + expect(work_items).to contain_exactly(work_item) + expect(results.work_items_count).to eq 1 + end + + context 'when on saas', :saas do + it 'does not return the work_item when searching with note text' do + expect(work_items).to be_empty + expect(results.work_items_count).to eq 0 + end + end + + context 'when search_work_item_queries_notes is false' do + before do + stub_feature_flags(search_work_item_queries_notes: false) + end + + it 'does not return the work_item when searching with note text' do + expect(work_items).to be_empty + expect(results.work_items_count).to eq 0 + end + end + + context 'when query source is GLQL' do + let(:source) { 'glql' } + + it 'does not return the work_item when searching with note text' do + expect(work_items).to be_empty + expect(results.work_items_count).to eq 0 + end + end + end +end diff --git a/ee/spec/services/ee/search/global_service_work_items_visibility_spec.rb b/ee/spec/services/ee/search/global_service_work_items_visibility_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..25f2e2cf7cd983580583d34435108a972c0984d1 --- /dev/null +++ b/ee/spec/services/ee/search/global_service_work_items_visibility_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Search::GlobalService, '#visibility', feature_category: :global_search do + include SearchResultHelpers + include ProjectHelpers + include UserHelpers + + before do + stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) + end + + describe 'visibility', :elastic_delete_by_query do + include_context 'ProjectPolicyTable context' + + let_it_be_with_reload(:group) { create(:group, :wiki_enabled) } + let_it_be_with_reload(:project) { create(:project, namespace: group) } + let(:projects) { [project] } + + let(:user) { create_user_from_membership(project, membership) } + let(:user_in_group) { create_user_from_membership(group, membership) } + + let(:scope) { 'work_items' } + + context 'for project level work items' do + let_it_be(:work_item) { create :work_item, project: project } + + let(:search) { work_item.title } + + where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do + permission_table_for_guest_feature_access + end + + with_them do + before do + Elastic::ProcessInitialBookkeepingService.track!(work_item) + ensure_elasticsearch_index! + end + + it_behaves_like 'search respects visibility' + end + end + + # TODO - add group level work items + end +end diff --git a/ee/spec/services/ee/search/group_service_work_items_visibility_spec.rb b/ee/spec/services/ee/search/group_service_work_items_visibility_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0070ccbc125c3bbb3270da0c505cb1014ab64aa2 --- /dev/null +++ b/ee/spec/services/ee/search/group_service_work_items_visibility_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Search::GroupService, '#visibility', feature_category: :global_search do + include SearchResultHelpers + include ProjectHelpers + include UserHelpers + + before do + stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) + end + + describe 'visibility', :elastic_delete_by_query, :sidekiq_inline do + include_context 'ProjectPolicyTable context' + + let_it_be_with_refind(:group) { create(:group) } + let_it_be_with_refind(:project) { create(:project, group: group) } + let_it_be_with_refind(:project2) { create(:project) } + + let(:user) { create_user_from_membership(project, membership) } + let(:user_in_group) { create_user_from_membership(group, membership) } + + let(:projects) { [project, project2] } + let(:search_level) { group } + + context 'for project level work items' do + let_it_be(:work_item) { create(:work_item, project: project) } + let_it_be(:work_item2) { create(:work_item, project: project2, title: work_item.title) } + + let(:scope) { 'work_items' } + let(:search) { work_item.title } + + where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do + permission_table_for_guest_feature_access + end + + with_them do + before do + Elastic::ProcessInitialBookkeepingService.track!(work_item, work_item2) + ensure_elasticsearch_index! + end + + it_behaves_like 'search respects visibility' + end + end + + # TODO - add group level work items + end +end diff --git a/ee/spec/services/ee/search/project_service_work_items_visibility_spec.rb b/ee/spec/services/ee/search/project_service_work_items_visibility_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..bcec6a1243929b05b44b53e55ca9e45d2f3f4fa0 --- /dev/null +++ b/ee/spec/services/ee/search/project_service_work_items_visibility_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Search::ProjectService, '#visibility', feature_category: :global_search do + include SearchResultHelpers + include ProjectHelpers + include UserHelpers + + describe 'visibility', :elastic_delete_by_query, :sidekiq_inline do + include_context 'ProjectPolicyTable context' + + before do + stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) + end + + context 'for project level work items' do + let_it_be_with_reload(:group) { create(:group) } + let_it_be_with_reload(:project) { create(:project, group: group) } + let(:projects) { [project] } + let(:search_level) { project } + + let_it_be(:work_item) { create :work_item, project: project } + + let(:user_in_group) { create_user_from_membership(group, membership) } + let(:user) { create_user_from_membership(project, membership) } + + let(:scope) { 'work_items' } + let(:search) { work_item.title } + + where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do + permission_table_for_guest_feature_access + end + + with_them do + before do + Elastic::ProcessInitialBookkeepingService.track!(work_item) + ensure_elasticsearch_index! + end + + it_behaves_like 'search respects visibility' + end + end + end +end diff --git a/ee/spec/support/shared_examples/services/search_service_shared_examples.rb b/ee/spec/support/shared_examples/services/search_service_shared_examples.rb index a530b9bccac855c2344d140aae92e82043f56bf3..281bdf41ca33a56d3fe4ea13db13a483f60c04fc 100644 --- a/ee/spec/support/shared_examples/services/search_service_shared_examples.rb +++ b/ee/spec/support/shared_examples/services/search_service_shared_examples.rb @@ -172,6 +172,8 @@ def create_records!(type) create_list(:work_item, records_count, :group_level, :epic_with_legacy_epic, namespace: searched_group) when 'issues' create_list(:issue, records_count, project: searched_project) + when 'work_items' + create_list(:work_item, records_count, project: searched_project) when 'merge_requests' records = [] records_count.times { |i| records << create(:merge_request, source_branch: i, source_project: searched_project) } diff --git a/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb index 68d259a3c354e0f30873caf9202a6e70e27c1e2c..1b26b8e7db3c142d501e5984b3677c6ca0438490 100644 --- a/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb @@ -1,25 +1,30 @@ # frozen_string_literal: true -RSpec.shared_examples 'access restricted confidential issues' do |document_type: :issue| - let(:query) { 'issue' } +RSpec.shared_examples 'access restricted confidential objects' do |document_type: :issue| + let(:query) { 'object' } let(:author) { create(:user) } let(:assignee) { create(:user) } let(:project) { create(:project, :internal) } - let!(:issue) { create(document_type, project: project, title: 'Issue 1') } - let!(:security_issue_1) { create(document_type, :confidential, project: project, title: 'Security issue 1', author: author) } - let!(:security_issue_2) { create(document_type, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) } + let!(:object) { create(document_type, project: project, title: 'object 1') } + let!(:security_object_1) do + create(document_type, :confidential, project: project, title: 'Security object 1', author: author) + end + + let!(:security_object_2) do + create(document_type, :confidential, title: 'Security object 2', project: project, assignees: [assignee]) + end subject(:objects) do - described_class.new(user, query, project: project).objects('issues') + described_class.new(user, query, project: project).objects(scope) end context 'when the user is non-member' do let(:user) { create(:user) } - it 'does not list project confidential issues for non project members' do - expect(objects).to contain_exactly(issue) - expect(results.limited_issues_count).to eq 1 + it 'does not list project confidential objects for non project members' do + expect(objects).to contain_exactly(object) + expect(results.public_send(:"limited_#{scope}_count")).to eq 1 end end @@ -28,27 +33,27 @@ create(:user) { |guest| project.add_guest(guest) } end - it 'does not list project confidential issues for project members with guest role' do - expect(objects).to contain_exactly(issue) - expect(results.limited_issues_count).to eq 1 + it 'does not list project confidential objects for project members with guest role' do + expect(objects).to contain_exactly(object) + expect(results.public_send(:"limited_#{scope}_count")).to eq 1 end end context 'when the user is the author' do let(:user) { author } - it 'lists project confidential issues' do - expect(objects).to contain_exactly(issue, security_issue_1) - expect(results.limited_issues_count).to eq 2 + it 'lists project confidential objects' do + expect(objects).to contain_exactly(object, security_object_1) + expect(results.public_send(:"limited_#{scope}_count")).to eq 2 end end context 'when the user is the assignee' do let(:user) { assignee } - it 'lists project confidential issues for assignee' do - expect(objects).to contain_exactly(issue, security_issue_2) - expect(results.limited_issues_count).to eq 2 + it 'lists project confidential objects for assignee' do + expect(objects).to contain_exactly(object, security_object_2) + expect(results.public_send(:"limited_#{scope}_count")).to eq 2 end end @@ -57,9 +62,9 @@ create(:user) { |user| project.add_developer(user) } end - it 'lists project confidential issues' do - expect(objects).to contain_exactly(issue, security_issue_1, security_issue_2) - expect(results.limited_issues_count).to eq 3 + it 'lists project confidential objects' do + expect(objects).to contain_exactly(object, security_object_1, security_object_2) + expect(results.public_send(:"limited_#{scope}_count")).to eq 3 end end @@ -67,15 +72,15 @@ let(:user) { create(:user, admin: true) } context 'when admin mode is enabled', :enable_admin_mode do - it 'lists all project issues' do - expect(objects).to contain_exactly(issue, security_issue_1, security_issue_2) + it 'lists all project objects' do + expect(objects).to contain_exactly(object, security_object_1, security_object_2) end end context 'when admin mode is disabled' do - it 'does not list project confidential issues' do - expect(objects).to contain_exactly(issue) - expect(results.limited_issues_count).to eq 1 + it 'does not list project confidential objects' do + expect(objects).to contain_exactly(object) + expect(results.public_send(:"limited_#{scope}_count")).to eq 1 end end end diff --git a/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb index e70dfec80b13a59731bb4fad5292bbbbef18252d..8246d0001e296e94dfba8765b9f557c0f13f60cd 100644 --- a/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb @@ -5,8 +5,8 @@ let(:filters) { {} } it 'returns confidential and not confidential results', :aggregate_failures do - expect(results.objects('issues')).to include confidential_result - expect(results.objects('issues')).to include opened_result + expect(results.objects(scope)).to include confidential_result + expect(results.objects(scope)).to include opened_result end end @@ -14,8 +14,8 @@ let(:filters) { { confidential: true } } it 'returns only confidential results', :aggregate_failures do - expect(results.objects('issues')).to include confidential_result - expect(results.objects('issues')).not_to include opened_result + expect(results.objects(scope)).to include confidential_result + expect(results.objects(scope)).not_to include opened_result end end @@ -23,8 +23,8 @@ let(:filters) { { confidential: false } } it 'returns not confidential results', :aggregate_failures do - expect(results.objects('issues')).not_to include confidential_result - expect(results.objects('issues')).to include opened_result + expect(results.objects(scope)).not_to include confidential_result + expect(results.objects(scope)).to include opened_result end end end diff --git a/spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb index 6965ca53981720660879e5e1e12a6449018f58bf..8c636887901d4bc8840d7642d573d7f655b07bfb 100644 --- a/spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb @@ -1,13 +1,16 @@ # frozen_string_literal: true -RSpec.shared_examples 'search results filtered by labels' do +RSpec.shared_examples 'search results filtered by labels' do |document_type: :issue| let_it_be(:project_label) { create(:label, project: project) } - let_it_be(:labeled_issue) { create(:labeled_issue, labels: [project_label], project: project, title: 'foo project') } - let_it_be(:unlabeled_issue) { create(:issue, project: project, title: 'foo unlabeled') } + let_it_be(:labeled_object) do + create(document_type, labels: [project_label], project: project, title: 'foo project') + end + + let_it_be(:unlabeled_object) { create(document_type, project: project, title: 'foo unlabeled') } before do - ::Elastic::ProcessBookkeepingService.track!(labeled_issue) - ::Elastic::ProcessBookkeepingService.track!(unlabeled_issue) + ::Elastic::ProcessBookkeepingService.track!(labeled_object) + ::Elastic::ProcessBookkeepingService.track!(unlabeled_object) ensure_elasticsearch_index! end @@ -15,7 +18,7 @@ let(:filters) { { label_name: [project_label.name] } } it 'filters by labels', :sidekiq_inline do - expect(results.objects(scope)).to contain_exactly(labeled_issue) + expect(results.objects(scope)).to contain_exactly(labeled_object) end end end