Commit 89841ca8 authored by Dylan Griffith's avatar Dylan Griffith

Merge branch '225998-advanced-search-result-page-is-slow' into 'master'

Optimize the ES Query for confidentiality check

Closes #225998

See merge request gitlab-org/gitlab!38095
parents 85d4e14b 62aaaa1b
......@@ -5,16 +5,15 @@ module Search
include Gitlab::Utils::StrongMemoize
attr_accessor :current_user, :params
attr_reader :default_project_filter
def initialize(user, params)
@current_user, @params = user, params.dup
@default_project_filter = true
end
def execute
Gitlab::SearchResults.new(current_user, projects, params[:search],
default_project_filter: default_project_filter)
Gitlab::SearchResults.new(current_user,
params[:search],
projects)
end
def projects
......
......@@ -7,13 +7,15 @@ module Search
def initialize(user, group, params)
super(user, params)
@default_project_filter = false
@group = group
end
def execute
Gitlab::GroupSearchResults.new(
current_user, projects, group, params[:search], default_project_filter: default_project_filter
current_user,
params[:search],
projects,
group: group
)
end
......
......@@ -10,9 +10,9 @@ module Search
def execute
Gitlab::ProjectSearchResults.new(current_user,
project,
params[:search],
params[:repository_ref])
project: project,
repository_ref: params[:repository_ref])
end
def scope
......
......@@ -14,9 +14,8 @@ module EE
::Gitlab::Elastic::SearchResults.new(
current_user,
params[:search],
elastic_projects,
projects,
elastic_global
public_and_internal_projects: elastic_global
)
end
......@@ -24,18 +23,6 @@ module EE
nil
end
def elastic_projects
strong_memoize(:elastic_projects) do
if current_user&.can_read_all_resources?
:any
elsif current_user
current_user.authorized_projects.pluck(:id) # rubocop: disable CodeReuse/ActiveRecord
else
[]
end
end
end
def elastic_global
true
end
......
......@@ -10,11 +10,6 @@ module EE
group
end
override :elastic_projects
def elastic_projects
@elastic_projects ||= projects.pluck(:id) # rubocop:disable CodeReuse/ActiveRecord
end
override :elastic_global
def elastic_global
false
......@@ -26,12 +21,10 @@ module EE
::Gitlab::Elastic::GroupSearchResults.new(
current_user,
elastic_projects,
projects,
group,
params[:search],
elastic_global,
default_project_filter: default_project_filter
projects,
group: group,
public_and_internal_projects: elastic_global
)
end
end
......
......@@ -13,8 +13,8 @@ module EE
::Gitlab::Elastic::ProjectSearchResults.new(
current_user,
params[:search],
project,
repository_ref
project: project,
repository_ref: repository_ref
)
end
......
......@@ -9,7 +9,9 @@ module EE
def execute
return super unless use_elasticsearch?
::Gitlab::Elastic::SnippetSearchResults.new(current_user, params[:search], elastic_projects, nil, true)
::Gitlab::Elastic::SnippetSearchResults.new(current_user,
params[:search],
projects)
end
override :elasticsearchable_scope
......
---
title: Optimize the Advanced Search query for Issues and Notes.
merge_request: 38095
author:
type: performance
......@@ -119,22 +119,20 @@ module Elastic
# documents gated by that project feature - e.g., "issues". The feature's
# visibility level must be taken into account.
def project_ids_query(user, project_ids, public_and_internal_projects, features = nil)
# When reading cross project is not allowed, only allow searching a
# a single project, so the `:read_*` ability is only checked once.
unless Ability.allowed?(user, :read_cross_project)
project_ids = [] if project_ids.is_a?(Array) && project_ids.size > 1
end
scoped_project_ids = scoped_project_ids(user, project_ids)
# At least one condition must be present, so pick no projects for
# anonymous users.
# Pick private, internal and public projects the user is a member of.
# Pick all private projects for admins & auditors.
conditions = pick_projects_by_membership(project_ids, user, features)
conditions = pick_projects_by_membership(scoped_project_ids, user, features)
if public_and_internal_projects
# Skip internal projects for anonymous and external users.
# Others are given access to all internal projects. Admins & auditors
# get access to internal projects where the feature is private.
# Others are given access to all internal projects.
#
# Admins & auditors get access to internal projects even
# if the feature is private.
conditions += pick_projects_by_visibility(Project::INTERNAL, user, features) if user && !user.external?
# All users, including anonymous, can access public projects.
......@@ -219,6 +217,33 @@ module Elastic
.filter_by_feature_visibility(feature, user)
.pluck_primary_key
end
def scoped_project_ids(current_user, project_ids)
return :any if project_ids == :any
project_ids ||= []
# When reading cross project is not allowed, only allow searching a
# a single project, so the `:read_*` ability is only checked once.
unless Ability.allowed?(current_user, :read_cross_project)
return [] if project_ids.size > 1
end
project_ids
end
def authorized_project_ids(current_user, options = {})
return [] unless current_user
scoped_project_ids = scoped_project_ids(current_user, options[:project_ids])
authorized_project_ids = current_user.authorized_projects(Gitlab::Access::REPORTER).pluck_primary_key.to_set
# if the current search is limited to a subset of projects, we should do
# confidentiality check for these projects.
authorized_project_ids &= scoped_project_ids.to_set unless scoped_project_ids == :any
authorized_project_ids.to_a
end
end
end
end
......@@ -10,13 +10,15 @@ module Elastic
case type
when 'all'
results[:blobs] = search_blob(query, page: page, per: per, options: options)
results[:commits] = search_commit(query, page: page, per: per, options: options)
results[:wiki_blobs] = search_blob(query, type: 'wiki_blob', page: page, per: per, options: options)
results[:commits] = search_commit(query, page: page, per: per, options: options.merge(features: 'repository'))
results[:blobs] = search_blob(query, type: 'blob', page: page, per: per, options: options.merge(features: 'repository'))
results[:wiki_blobs] = search_blob(query, type: 'wiki_blob', page: page, per: per, options: options.merge(features: 'wiki'))
when 'commit'
results[:commits] = search_commit(query, page: page, per: per, options: options)
when 'blob', 'wiki_blob'
results[type.pluralize.to_sym] = search_blob(query, type: type, page: page, per: per, options: options)
results[:commits] = search_commit(query, page: page, per: per, options: options.merge(features: 'repository'))
when 'blob'
results[:blobs] = search_blob(query, type: type, page: page, per: per, options: options.merge(features: 'repository'))
when 'wiki_blob'
results[:wiki_blobs] = search_blob(query, type: type, page: page, per: per, options: options.merge(features: 'wiki'))
end
results
......@@ -54,12 +56,17 @@ module Elastic
bool_expr = Gitlab::Elastic::BoolExpr.new
query_hash = {
query: { bool: bool_expr },
size: per,
from: per * (page - 1),
sort: [:_score]
}
# If there is a :current_user set in the `options`, we can assume
# we need to do a project visibility check.
#
# Note that `:current_user` might be `nil` for a anonymous user
query_hash = project_ids_filter(query_hash, options) if options.key?(:current_user)
if query.blank?
bool_expr[:must] = { match_all: {} }
query_hash[:track_scores] = true
......@@ -73,11 +80,15 @@ module Elastic
}
end
options_filter_context = options_filter_context(:commit, options)
# add the document type filter
bool_expr[:filter] << { term: { type: 'commit' } }
# add filters extracted from the options
options_filter_context = options_filter_context(:commit, options)
bool_expr[:filter] += options_filter_context[:filter] if options_filter_context[:filter].any?
options[:order] = :default if options[:order].blank?
if options[:highlight]
es_fields = fields.map { |field| field.split('^').first }.each_with_object({}) do |field, memo|
memo[field.to_sym] = {}
......@@ -90,8 +101,6 @@ module Elastic
}
end
options[:order] = :default if options[:order].blank?
res = search(query_hash, options)
{
results: res.results,
......@@ -125,6 +134,12 @@ module Elastic
}
}
# If there is a :current_user set in the `options`, we can assume
# we need to do a project visibility check.
#
# Note that `:current_user` might be `nil` for a anonymous user
query_hash = project_ids_filter(query_hash, options) if options.key?(:current_user)
# add the document type filter
bool_expr[:filter] << { term: { type: type } }
......
......@@ -13,29 +13,32 @@ module Elastic
options[:features] = 'issues'
query_hash = project_ids_filter(query_hash, options)
query_hash = confidentiality_filter(query_hash, options[:current_user], options[:project_ids])
query_hash = confidentiality_filter(query_hash, options)
search(query_hash, options)
end
private
def user_has_access_to_confidential_issues?(authorized_project_ids, project_ids)
# is_a?(Array) is needed because we might receive project_ids: :any
return false unless authorized_project_ids && project_ids.is_a?(Array)
def confidentiality_filter(query_hash, options)
current_user = options[:current_user]
project_ids = options[:project_ids]
(project_ids - authorized_project_ids).empty?
end
def confidentiality_filter(query_hash, current_user, project_ids)
return query_hash if current_user&.can_read_all_resources?
authorized_project_ids = current_user&.authorized_projects(Gitlab::Access::REPORTER)&.pluck_primary_key
return query_hash if user_has_access_to_confidential_issues?(authorized_project_ids, project_ids)
scoped_project_ids = scoped_project_ids(current_user, project_ids)
authorized_project_ids = authorized_project_ids(current_user, options)
# we can shortcut the filter if the user is authorized to see
# all the projects for which this query is scoped on
unless scoped_project_ids == :any || scoped_project_ids.empty?
return query_hash if authorized_project_ids.to_set == scoped_project_ids.to_set
end
filter =
if current_user
{
filter = { term: { confidential: false } }
if current_user
filter = {
bool: {
should: [
{ term: { confidential: false } },
......@@ -58,9 +61,7 @@ module Elastic
]
}
}
else
{ term: { confidential: false } }
end
end
query_hash[:query][:bool][:filter] << filter
query_hash
......
......@@ -14,7 +14,7 @@ module Elastic
query_hash = basic_query_hash(%w[note], query)
query_hash = project_ids_filter(query_hash, options)
query_hash = confidentiality_filter(query_hash, options[:current_user])
query_hash = confidentiality_filter(query_hash, options)
query_hash[:highlight] = highlight_options(options[:in])
......@@ -23,7 +23,9 @@ module Elastic
private
def confidentiality_filter(query_hash, current_user)
def confidentiality_filter(query_hash, options)
current_user = options[:current_user]
return query_hash if current_user&.can_read_all_resources?
filter = {
......@@ -43,7 +45,7 @@ module Elastic
bool: {
should: [
{ bool: { must_not: [{ exists: { field: :confidential } }] } },
{ term: { "confidential" => false } }
{ term: { confidential: false } }
]
}
}
......@@ -61,7 +63,7 @@ module Elastic
bool: {
should: [
{ term: { "issue.confidential" => true } },
{ term: { "confidential" => true } }
{ term: { confidential: true } }
]
}
},
......@@ -70,7 +72,7 @@ module Elastic
should: [
{ term: { "issue.author_id" => current_user.id } },
{ term: { "issue.assignee_id" => current_user.id } },
{ terms: { "project_id" => current_user.authorized_projects(Gitlab::Access::REPORTER).pluck_primary_key } }
{ terms: { project_id: authorized_project_ids(current_user, options) } }
]
}
}
......
......@@ -11,15 +11,20 @@ module Gitlab
attr_reader :group, :default_project_filter
def initialize(current_user, limit_project_ids, limit_projects, group, query, public_and_internal_projects, default_project_filter: false)
super(current_user, query, limit_project_ids, limit_projects, public_and_internal_projects)
@default_project_filter = default_project_filter
def initialize(current_user, query, limit_projects = nil, group:, public_and_internal_projects: false, default_project_filter: false)
@group = group
@default_project_filter = default_project_filter
super(current_user, query, limit_projects, public_and_internal_projects: public_and_internal_projects)
end
def generic_search_results
@generic_search_results ||= Gitlab::GroupSearchResults.new(current_user, limit_projects, group, query, default_project_filter: default_project_filter)
@generic_search_results ||= Gitlab::GroupSearchResults.new(
current_user,
query,
limit_projects,
group: group
)
end
end
end
......
......@@ -11,16 +11,20 @@ module Gitlab
delegate :users, to: :generic_search_results
delegate :limited_users_count, to: :generic_search_results
def initialize(current_user, query, project, repository_ref = nil)
@current_user = current_user
def initialize(current_user, query, project:, repository_ref: nil)
@project = project
@repository_ref = repository_ref.presence || project.default_branch
@query = query
@public_and_internal_projects = false
super(current_user, query, [project], public_and_internal_projects: false)
end
def generic_search_results
@generic_search_results ||= Gitlab::ProjectSearchResults.new(current_user, project, query, repository_ref)
@generic_search_results ||= Gitlab::ProjectSearchResults.new(
current_user,
query,
project: project,
repository_ref: repository_ref
)
end
private
......@@ -95,10 +99,6 @@ module Gitlab
end
end
def limit_project_ids
[project.id]
end
def root_ref?
project.root_ref?(repository_ref)
end
......
......@@ -9,18 +9,17 @@ module Gitlab
attr_reader :current_user, :query, :public_and_internal_projects
# Limit search results by passed project ids
# Limit search results by passed projects
# It allows us to search only for projects user has access to
attr_reader :limit_project_ids, :limit_projects
attr_reader :limit_projects
delegate :users, to: :generic_search_results
delegate :limited_users_count, to: :generic_search_results
def initialize(current_user, query, limit_project_ids, limit_projects = nil, public_and_internal_projects = true)
def initialize(current_user, query, limit_projects = nil, public_and_internal_projects: true)
@current_user = current_user
@limit_project_ids = limit_project_ids
@limit_projects = limit_projects
@query = query
@limit_projects = limit_projects
@public_and_internal_projects = public_and_internal_projects
end
......@@ -52,7 +51,7 @@ module Gitlab
end
def generic_search_results
@generic_search_results ||= Gitlab::SearchResults.new(current_user, limit_projects, query)
@generic_search_results ||= Gitlab::SearchResults.new(current_user, query, limit_projects)
end
def formatted_count(scope)
......@@ -110,6 +109,17 @@ module Gitlab
@milestones_count ||= milestones.total_count
end
# mbergeron: these aliases act as an adapter to the Gitlab::SearchResults
# interface, which is mostly implemented by this class.
alias_method :limited_projects_count, :projects_count
alias_method :limited_notes_count, :notes_count
alias_method :limited_blobs_count, :blobs_count
alias_method :limited_wiki_blobs_count, :wiki_blobs_count
alias_method :limited_commits_count, :commits_count
alias_method :limited_issues_count, :issues_count
alias_method :limited_merge_requests_count, :merge_requests_count
alias_method :limited_milestones_count, :milestones_count
def single_commit_result?
false
end
......@@ -165,6 +175,20 @@ module Gitlab
private
# Convert the `limit_projects` to a list of ids for Elasticsearch
def limit_project_ids
strong_memoize(:limit_project_ids) do
case limit_projects
when :any then :any
when ActiveRecord::Relation
limit_projects.pluck_primary_key if limit_projects.model == Project
when Array
limit_projects.all? { |x| x.is_a?(Project) } ? limit_projects.map(&:id) : []
else []
end
end
end
# Apply some eager loading to the `records` of an ES result object without
# losing pagination information. Also, take advantage of preload method if
# provided by the caller.
......@@ -216,11 +240,7 @@ module Gitlab
def merge_requests
strong_memoize(:merge_requests) do
options = base_options.merge(
project_ids: filter_project_ids_by_feature(:merge_requests, limit_project_ids)
)
MergeRequest.elastic_search(query, options: options)
MergeRequest.elastic_search(query, options: base_options)
end
end
......@@ -234,15 +254,11 @@ module Gitlab
return Kaminari.paginate_array([]) if query.blank?
strong_memoize(:blobs) do
options = base_options.merge(
additional_filter: repository_filter(limit_project_ids)
)
Repository.__elasticsearch__.elastic_search_as_found_blob(
query,
page: (page || 1).to_i,
per: per_page,
options: options
options: base_options
)
end
end
......@@ -251,15 +267,11 @@ module Gitlab
return Kaminari.paginate_array([]) if query.blank?
strong_memoize(:wiki_blobs) do
options = base_options.merge(
additional_filter: wiki_filter(limit_project_ids)
)
ProjectWiki.__elasticsearch__.elastic_search_as_wiki_page(
query,
page: (page || 1).to_i,
per: per_page,
options: options
options: base_options
)
end
end
......@@ -274,104 +286,16 @@ module Gitlab
return Kaminari.paginate_array([]) if query.blank?
strong_memoize(:commits) do
options = base_options.merge(
additional_filter: repository_filter(limit_project_ids)
)
Repository.find_commits_by_message_with_elastic(
query,
page: (page || 1).to_i,
per_page: per_page,
options: options,
options: base_options,
preload_method: preload_method
)
end
end
def wiki_filter(project_ids)
blob_filter(:wiki, project_ids)
end
def repository_filter(project_ids)
blob_filter(:repository, project_ids)
end
def filter_project_ids_by_feature(feature, project_ids)
return project_ids if project_ids == :any
Project
.id_in(project_ids)
.filter_by_feature_visibility(feature, current_user)
.pluck_primary_key
end
def blob_filter(feature, project_ids)
key_name = "#{feature}_access_level"
project_ids = filter_project_ids_by_feature(feature, project_ids)
conditions =
if project_ids == :any
[{ exists: { field: "id" } }]
else
[{ terms: { id: project_ids } }]
end
if public_and_internal_projects
conditions << {
bool: {
filter: [
{ term: { visibility_level: Project::PUBLIC } },
{ term: { key_name => ProjectFeature::ENABLED } }
]
}
}
if current_user && !current_user.external?
conditions << {
bool: {
filter: [
{ term: { visibility_level: Project::INTERNAL } },
{ term: { key_name => ProjectFeature::ENABLED } }
]
}
}
end
end
{
has_parent: {
parent_type: 'project',
query: {
bool: {
should: conditions,
must_not: { term: { key_name => ProjectFeature::DISABLED } }
}
}
}
}
end
# rubocop: disable CodeReuse/ActiveRecord
def guest_project_ids
if current_user
current_user.authorized_projects
.where('project_authorizations.access_level = ?', Gitlab::Access::GUEST)
.pluck(:id)
else
[]
end
end
# rubocop: enable CodeReuse/ActiveRecord
def non_guest_project_ids
if limit_project_ids == :any
:any
else
@non_guest_project_ids ||= limit_project_ids - guest_project_ids
end
end
def default_scope
'projects'
end
......
......@@ -21,7 +21,7 @@ RSpec.describe 'GlobalSearch', :elastic do
project.add_guest(guest)
end
context "Respect feature visibility levels" do
context "Respect feature visibility levels", :aggregate_failures do
context "Private projects" do
let(:project) { create(:project, :private, :repository, :wiki_repo) }
......@@ -127,7 +127,7 @@ RSpec.describe 'GlobalSearch', :elastic do
expect_items_to_be_found(nil)
end
it "shows items to member only if features are private" do
it "shows items to member only if features are private", :aggregate_failures do
create_items(project, feature_settings(:private))
expect_items_to_be_found(admin)
......@@ -178,9 +178,9 @@ RSpec.describe 'GlobalSearch', :elastic do
check_count = lambda do |feature, c|
if arr.include?(feature)
expect(c).to be > 0
expect(c).to be > 0, "Search returned no #{feature} for #{user}"
else
expect(c).to eq(0)
expect(c).to eq(0), "Search returned #{feature} for #{user}"
end
end
......
......@@ -7,7 +7,7 @@ RSpec.describe Gitlab::SearchResults do
let_it_be(:compliance_project) { create(:project, :with_compliance_framework, name: 'foo') }
subject { described_class.new(user, Project.all, 'foo') }
subject { described_class.new(user, 'foo') }
describe '#projects' do
it 'avoid N+1 queries' do
......
......@@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe Gitlab::Elastic::GroupSearchResults do
subject(:results) { described_class.new(user, nil, nil, group, query, nil) }
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:guest) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::GUEST) } }
subject(:results) { described_class.new(user, query, group: group) }
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end
......@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Elastic::GroupSearchResults do
expect(Gitlab::GroupSearchResults).to receive(:new).and_call_original
end
it { expect(results.objects('users')).to eq([guest]) }
it { expect(results.objects('users')).to contain_exactly(guest) }
it { expect(results.limited_users_count).to eq(1) }
describe 'pagination' do
......@@ -29,8 +29,8 @@ RSpec.describe Gitlab::Elastic::GroupSearchResults do
let_it_be(:user2) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::REPORTER) } }
it 'returns the correct page of results' do
expect(results.objects('users', page: 1, per_page: 1)).to eq([user2])
expect(results.objects('users', page: 2, per_page: 1)).to eq([guest])
expect(results.objects('users', page: 1, per_page: 1)).to contain_exactly(user2)
expect(results.objects('users', page: 2, per_page: 1)).to contain_exactly(guest)
end
it 'returns the correct number of results for one page' do
......
......@@ -6,13 +6,16 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:query) { 'hello world' }
let(:repository_ref) { nil }
subject(:results) { described_class.new(user, query, project: project, repository_ref: repository_ref) }
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end
describe 'initialize with empty ref' do
subject(:results) { described_class.new(user, query, project, '') }
let(:repository_ref) { '' }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq('master') }
......@@ -20,58 +23,48 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do
end
describe 'initialize with ref' do
let(:ref) { 'refs/heads/test' }
subject(:results) { described_class.new(user, query, project, ref) }
let(:repository_ref) { 'refs/heads/test' }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) }
it { expect(results.repository_ref).to eq(repository_ref) }
it { expect(results.query).to eq('hello world') }
end
describe "search", :sidekiq_might_not_need_inline do
it "returns correct amounts" do
project = create :project, :public, :repository, :wiki_repo
project1 = create :project, :public, :repository, :wiki_repo
project.repository.index_commits_and_blobs
# Notes
create :note, note: 'bla-bla term', project: project
# The note in the project you have no access to
create :note, note: 'bla-bla term', project: project1
let(:project) { create(:project, :public, :repository, :wiki_repo) }
let(:private_project) { create(:project, :repository, :wiki_repo) }
# Wiki
project.wiki.create_page('index_page', 'term')
project.wiki.index_wiki_blobs
project1.wiki.create_page('index_page', ' term')
project1.wiki.index_wiki_blobs
before do
[project, private_project].each do |project|
create(:note, note: 'bla-bla term', project: project)
project.wiki.create_page('index_page', 'term')
project.wiki.index_wiki_blobs
end
project.repository.index_commits_and_blobs
ensure_elasticsearch_index!
end
result = described_class.new(user, 'term', project)
it "returns correct amounts" do
result = described_class.new(user, 'term', project: project)
expect(result.notes_count).to eq(1)
expect(result.wiki_blobs_count).to eq(1)
expect(result.blobs_count).to eq(1)
result1 = described_class.new(user, 'initial', project)
expect(result1.commits_count).to eq(1)
result = described_class.new(user, 'initial', project: project)
expect(result.commits_count).to eq(1)
end
context 'visibility checks' do
it 'shows wiki for guests' do
project = create :project, :public, :wiki_repo
guest = create :user
project.add_guest(guest)
# Wiki
project.wiki.create_page('index_page', 'term')
project.wiki.index_wiki_blobs
let(:project) { create(:project, :public, :wiki_repo) }
let(:query) { 'term' }
ensure_elasticsearch_index!
before do
project.add_guest(user)
end
result = described_class.new(guest, 'term', project)
expect(result.wiki_blobs_count).to eq(1)
it 'shows wiki for guests' do
expect(results.wiki_blobs_count).to eq(1)
end
end
end
......@@ -79,12 +72,13 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do
describe "search for commits in non-default branch" do
let(:project) { create(:project, :public, :repository, visibility) }
let(:visibility) { :repository_enabled }
let(:result) { described_class.new(user, 'initial', project, 'test') }
let(:query) { 'initial' }
let(:repository_ref) { 'test' }
subject(:commits) { result.objects('commits') }
subject(:commits) { results.objects('commits') }
it 'finds needed commit' do
expect(result.commits_count).to eq(1)
expect(results.commits_count).to eq(1)
end
it 'responds to total_pages method' do
......@@ -122,9 +116,10 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do
describe 'search for blobs in non-default branch' do
let(:project) { create(:project, :public, :repository, :repository_private) }
let(:result) { described_class.new(user, 'initial', project, 'test') }
let(:query) { 'initial' }
let(:repository_ref) { 'test' }
subject(:blobs) { result.objects('blobs') }
subject(:blobs) { results.objects('blobs') }
it 'always returns zero results' do
expect_any_instance_of(Gitlab::FileFinder).to receive(:find).never
......@@ -134,88 +129,14 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do
end
describe 'confidential issues', :sidekiq_might_not_need_inline do
let(:query) { 'issue' }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
let(:admin) { create(:admin) }
let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) }
before do
ensure_elasticsearch_index!
end
it 'does not list project confidential issues for non project members' do
results = described_class.new(non_member, query, project)
issues = results.objects('issues')
expect(issues).to include issue
expect(issues).not_to include security_issue_1
expect(issues).not_to include security_issue_2
expect(results.issues_count).to eq 1
end
it 'lists project confidential issues for author' do
results = described_class.new(author, query, project)
issues = results.objects('issues')
expect(issues).to include issue
expect(issues).to include security_issue_1
expect(issues).not_to include security_issue_2
expect(results.issues_count).to eq 2
end
it 'lists project confidential issues for assignee' do
results = described_class.new(assignee, query, project)
issues = results.objects('issues')
expect(issues).to include issue
expect(issues).not_to include security_issue_1
expect(issues).to include security_issue_2
expect(results.issues_count).to eq 2
end
it 'lists project confidential issues for project members' do
project.add_developer(member)
results = described_class.new(member, query, project)
issues = results.objects('issues')
expect(issues).to include issue
expect(issues).to include security_issue_1
expect(issues).to include security_issue_2
expect(results.issues_count).to eq 3
end
it 'does not list project confidential issues for project members with guest role' do
project.add_guest(member)
results = described_class.new(member, query, project)
issues = results.objects('issues')
expect(issues).to include issue
expect(issues).not_to include security_issue_1
expect(issues).not_to include security_issue_2
expect(results.issues_count).to eq 1
end
it 'lists all project issues for admin' do
results = described_class.new(admin, query, project)
issues = results.objects('issues')
expect(issues).to include issue
expect(issues).to include security_issue_1
expect(issues).to include security_issue_2
expect(results.issues_count).to eq 3
include_examples 'access restricted confidential issues' do
before do
ensure_elasticsearch_index!
end
end
end
context 'user search' do
subject(:results) { described_class.new(user, query, project) }
let(:query) { project.owner.username }
before do
......@@ -243,6 +164,7 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do
context 'query performance' do
let(:project) { create(:project, :public, :repository, :wiki_repo) }
let(:query) { '*' }
before do
# wiki_blobs method checks to see if there is a wiki page before doing
......@@ -250,8 +172,6 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do
create(:wiki_page, wiki: project.wiki)
end
let(:results) { described_class.new(user, '*', project) }
include_examples 'does not hit Elasticsearch twice for objects and counts', %w|notes blobs wiki_blobs commits issues merge_requests milestones|
end
end
......@@ -10,12 +10,12 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
let(:user) { create(:user) }
let(:project_1) { create(:project, :public, :repository, :wiki_repo) }
let(:project_2) { create(:project, :public, :repository, :wiki_repo) }
let(:limit_project_ids) { [project_1.id] }
let(:limit_projects) { [project_1] }
describe '#formatted_count' do
using RSpec::Parameterized::TableSyntax
let(:results) { described_class.new(user, 'hello world', limit_project_ids) }
let(:results) { described_class.new(user, 'hello world', limit_projects) }
where(:scope, :count_method, :expected) do
'projects' | :projects_count | '1234'
......@@ -43,7 +43,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
shared_examples_for 'a paginated object' do |object_type|
let(:results) { described_class.new(user, 'hello world', limit_project_ids) }
let(:results) { described_class.new(user, 'hello world', limit_projects) }
it 'does not explode when given a page as a string' do
expect { results.objects(object_type, page: "2") }.not_to raise_error
......@@ -135,7 +135,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
it_behaves_like 'a paginated object', 'issues'
it 'lists found issues' do
results = described_class.new(user, 'hello world', limit_project_ids)
results = described_class.new(user, 'hello world', limit_projects)
issues = results.objects('issues')
expect(issues).to include @issue_1
......@@ -145,14 +145,14 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'returns empty list when issues are not found' do
results = described_class.new(user, 'security', limit_project_ids)
results = described_class.new(user, 'security', limit_projects)
expect(results.objects('issues')).to be_empty
expect(results.issues_count).to eq 0
end
it 'lists issue when search by a valid iid' do
results = described_class.new(user, '#2', limit_project_ids, nil, false)
results = described_class.new(user, '#2', limit_projects, public_and_internal_projects: false)
issues = results.objects('issues')
expect(issues).not_to include @issue_1
......@@ -162,7 +162,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'returns empty list when search by invalid iid' do
results = described_class.new(user, '#222', limit_project_ids)
results = described_class.new(user, '#222', limit_projects)
expect(results.objects('issues')).to be_empty
expect(results.issues_count).to eq 0
......@@ -198,7 +198,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
it_behaves_like 'a paginated object', 'notes'
it 'lists found notes' do
results = described_class.new(user, 'foo', limit_project_ids)
results = described_class.new(user, 'foo', limit_projects)
notes = results.objects('notes')
expect(notes).to include @note_1
......@@ -208,7 +208,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'returns empty list when notes are not found' do
results = described_class.new(user, 'security', limit_project_ids)
results = described_class.new(user, 'security', limit_projects)
expect(results.objects('notes')).to be_empty
expect(results.notes_count).to eq 0
......@@ -218,7 +218,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
describe 'confidential issues' do
let(:project_3) { create(:project, :public) }
let(:project_4) { create(:project, :public) }
let(:limit_project_ids) { [project_1.id, project_2.id, project_3.id] }
let(:limit_projects) { [project_1, project_2, project_3] }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
......@@ -240,7 +240,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
let(:query) { 'issue' }
it 'does not list confidential issues for guests' do
results = described_class.new(nil, query, limit_project_ids)
results = described_class.new(nil, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include @issue
......@@ -253,7 +253,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'does not list confidential issues for non project members' do
results = described_class.new(non_member, query, limit_project_ids)
results = described_class.new(non_member, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include @issue
......@@ -266,7 +266,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'lists confidential issues for author' do
results = described_class.new(author, query, limit_project_ids)
results = described_class.new(author, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include @issue
......@@ -279,7 +279,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'lists confidential issues for assignee' do
results = described_class.new(assignee, query, limit_project_ids)
results = described_class.new(assignee, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include @issue
......@@ -295,7 +295,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
project_1.add_developer(member)
project_2.add_developer(member)
results = described_class.new(member, query, limit_project_ids)
results = described_class.new(member, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include @issue
......@@ -308,7 +308,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'lists all issues for admin' do
results = described_class.new(admin, query, limit_project_ids)
results = described_class.new(admin, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include @issue
......@@ -325,7 +325,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
let(:query) { '#1' }
it 'does not list confidential issues for guests' do
results = described_class.new(nil, query, limit_project_ids)
results = described_class.new(nil, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include @issue
......@@ -338,7 +338,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'does not list confidential issues for non project members' do
results = described_class.new(non_member, query, limit_project_ids)
results = described_class.new(non_member, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include @issue
......@@ -351,7 +351,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'lists confidential issues for author' do
results = described_class.new(author, query, limit_project_ids)
results = described_class.new(author, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include @issue
......@@ -364,7 +364,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'lists confidential issues for assignee' do
results = described_class.new(assignee, query, limit_project_ids)
results = described_class.new(assignee, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include @issue
......@@ -380,7 +380,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
project_2.add_developer(member)
project_3.add_developer(member)
results = described_class.new(member, query, limit_project_ids)
results = described_class.new(member, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include @issue
......@@ -393,7 +393,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'lists all issues for admin' do
results = described_class.new(admin, query, limit_project_ids)
results = described_class.new(admin, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include @issue
......@@ -439,7 +439,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
it_behaves_like 'a paginated object', 'merge_requests'
it 'lists found merge requests' do
results = described_class.new(user, 'hello world', limit_project_ids)
results = described_class.new(user, 'hello world', limit_projects)
merge_requests = results.objects('merge_requests')
expect(merge_requests).to include @merge_request_1
......@@ -449,14 +449,14 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'returns empty list when merge requests are not found' do
results = described_class.new(user, 'security', limit_project_ids)
results = described_class.new(user, 'security', limit_projects)
expect(results.objects('merge_requests')).to be_empty
expect(results.merge_requests_count).to eq 0
end
it 'lists merge request when search by a valid iid' do
results = described_class.new(user, '#2', limit_project_ids)
results = described_class.new(user, '#2', limit_projects)
merge_requests = results.objects('merge_requests')
expect(merge_requests).not_to include @merge_request_1
......@@ -466,7 +466,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'returns empty list when search by invalid iid' do
results = described_class.new(user, '#222', limit_project_ids)
results = described_class.new(user, '#222', limit_projects)
expect(results.objects('merge_requests')).to be_empty
expect(results.merge_requests_count).to eq 0
......@@ -500,7 +500,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
ensure_elasticsearch_index!
result = described_class.new(user, 'term', [project.id])
result = described_class.new(user, 'term', [project])
expect(result.issues_count).to eq(2)
expect(result.merge_requests_count).to eq(2)
......@@ -517,13 +517,13 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
def search_for(term)
described_class.new(user, term, [project_1.id]).objects('blobs').map(&:path)
described_class.new(user, term, [project_1]).objects('blobs').map(&:path)
end
it_behaves_like 'a paginated object', 'blobs'
it 'finds blobs' do
results = described_class.new(user, 'def', limit_project_ids)
results = described_class.new(user, 'def', limit_projects)
blobs = results.objects('blobs')
expect(blobs.first.data).to include('def')
......@@ -531,7 +531,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
it 'finds blobs by prefix search' do
results = described_class.new(user, 'defau*', limit_project_ids)
results = described_class.new(user, 'defau*', limit_projects)
blobs = results.objects('blobs')
expect(blobs.first.data).to include('default')
......@@ -544,18 +544,18 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
project_2.add_reporter(user)
ensure_elasticsearch_index!
results = described_class.new(user, 'def', [project_1.id])
results = described_class.new(user, 'def', [project_1])
expect(results.blobs_count).to eq 5
result_project_ids = results.objects('blobs').map(&:project_id)
expect(result_project_ids.uniq).to eq([project_1.id])
results = described_class.new(user, 'def', [project_1.id, project_2.id])
results = described_class.new(user, 'def', [project_1, project_2])
expect(results.blobs_count).to eq 10
end
it 'returns zero when blobs are not found' do
results = described_class.new(user, 'asdfg', limit_project_ids)
results = described_class.new(user, 'asdfg', limit_projects)
expect(results.blobs_count).to eq 0
end
......@@ -720,7 +720,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
describe 'Wikis' do
let(:results) { described_class.new(user, 'term', limit_project_ids) }
let(:results) { described_class.new(user, 'term', limit_projects) }
subject(:wiki_blobs) { results.objects('wiki_blobs') }
......@@ -759,12 +759,12 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
expect(results.wiki_blobs_count).to eq 1
results = described_class.new(user, 'term', [project_1.id, project_2.id])
results = described_class.new(user, 'term', [project_1, project_2])
expect(results.wiki_blobs_count).to eq 2
end
it 'returns zero when wiki blobs are not found' do
results = described_class.new(user, 'asdfg', limit_project_ids)
results = described_class.new(user, 'asdfg', limit_projects)
expect(results.wiki_blobs_count).to eq 0
end
......@@ -773,13 +773,13 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
let(:project_1) { create(:project, :public, :repository, :wiki_disabled) }
context 'search by member' do
let(:limit_project_ids) { [project_1.id] }
let(:limit_projects) { [project_1] }
it { is_expected.to be_empty }
end
context 'search by non-member' do
let(:limit_project_ids) { [] }
let(:limit_projects) { [] }
it { is_expected.to be_empty }
end
......@@ -789,7 +789,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
let(:project_1) { create(:project, :public, :repository, :wiki_private, :wiki_repo) }
context 'search by member' do
let(:limit_project_ids) { [project_1.id] }
let(:limit_projects) { [project_1] }
before do
project_1.add_guest(user)
......@@ -799,7 +799,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
context 'search by non-member' do
let(:limit_project_ids) { [] }
let(:limit_projects) { [] }
it { is_expected.to be_empty }
end
......@@ -815,7 +815,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
it_behaves_like 'a paginated object', 'commits'
it 'finds commits' do
results = described_class.new(user, 'add', limit_project_ids)
results = described_class.new(user, 'add', limit_projects)
commits = results.objects('commits')
expect(commits.first.message.downcase).to include("add")
......@@ -828,15 +828,15 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
project_2.add_reporter(user)
ensure_elasticsearch_index!
results = described_class.new(user, 'add', [project_1.id])
results = described_class.new(user, 'add', [project_1])
expect(results.commits_count).to eq 24
results = described_class.new(user, 'add', [project_1.id, project_2.id])
results = described_class.new(user, 'add', [project_1, project_2])
expect(results.commits_count).to eq 48
end
it 'returns zero when commits are not found' do
results = described_class.new(user, 'asdfg', limit_project_ids)
results = described_class.new(user, 'asdfg', limit_projects)
expect(results.commits_count).to eq 0
end
......@@ -847,7 +847,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
let(:private_project1) { create(:project, :private, :repository, :wiki_repo, description: "Private project") }
let(:private_project2) { create(:project, :private, :repository, :wiki_repo, description: "Private project where I'm a member") }
let(:public_project) { create(:project, :public, :repository, :wiki_repo, description: "Public project") }
let(:limit_project_ids) { [private_project2.id] }
let(:limit_projects) { [private_project2] }
before do
private_project2.project_members.create(user: user, access_level: ProjectMember::DEVELOPER)
......@@ -863,7 +863,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
ensure_elasticsearch_index!
# Authenticated search
results = described_class.new(user, 'project', limit_project_ids)
results = described_class.new(user, 'project', limit_projects)
issues = results.objects('issues')
expect(issues).to include issue_1
......@@ -903,8 +903,8 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
internal_project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED)
ensure_elasticsearch_index!
project_ids = user.authorized_projects.pluck(:id)
results = described_class.new(user, 'project', project_ids)
projects = user.authorized_projects
results = described_class.new(user, 'project', projects)
milestones = results.objects('milestones')
expect(milestones).to match_array([milestone_1, milestone_3])
......@@ -930,8 +930,8 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
context 'when user can read milestones' do
it 'returns right set of milestones' do
# Authenticated search
project_ids = user.authorized_projects.pluck(:id)
results = described_class.new(user, 'project', project_ids)
projects = user.authorized_projects
results = described_class.new(user, 'project', projects)
milestones = results.objects('milestones')
expect(milestones).to match_array([milestone_1, milestone_3, milestone_4])
......@@ -1012,7 +1012,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
ensure_elasticsearch_index!
# Authenticated search
results = described_class.new(user, 'project', limit_project_ids)
results = described_class.new(user, 'project', limit_projects)
milestones = results.objects('projects')
expect(milestones).to include internal_project
......@@ -1039,7 +1039,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
ensure_elasticsearch_index!
# Authenticated search
results = described_class.new(user, 'project', limit_project_ids)
results = described_class.new(user, 'project', limit_projects)
merge_requests = results.objects('merge_requests')
expect(merge_requests).to include merge_request_1
......@@ -1068,7 +1068,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
it 'finds the right set of wiki blobs' do
# Authenticated search
results = described_class.new(user, 'term', limit_project_ids)
results = described_class.new(user, 'term', limit_projects)
blobs = results.objects('wiki_blobs')
expect(blobs.map(&:project)).to match_array [internal_project, private_project2, public_project]
......@@ -1100,7 +1100,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
ensure_elasticsearch_index!
# Authenticated search
results = described_class.new(user, 'search', limit_project_ids)
results = described_class.new(user, 'search', limit_projects)
commits = results.objects('commits')
expect(commits.map(&:project)).to match_array [internal_project, private_project2, public_project]
......@@ -1132,7 +1132,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
ensure_elasticsearch_index!
# Authenticated search
results = described_class.new(user, 'tesla', limit_project_ids)
results = described_class.new(user, 'tesla', limit_projects)
blobs = results.objects('blobs')
expect(blobs.map(&:project)).to match_array [internal_project, private_project2, public_project]
......@@ -1149,7 +1149,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
context 'query performance' do
let(:results) { described_class.new(user, 'hello world', limit_project_ids) }
let(:results) { described_class.new(user, 'hello world', limit_projects) }
include_examples 'does not hit Elasticsearch twice for objects and counts', %w|projects notes blobs wiki_blobs commits issues merge_requests milestones|
end
......
......@@ -17,15 +17,11 @@ module SearchResultHelpers
if expected_count
actual_count = results.public_send("#{target}_count")
expect(actual_count).to eq(expected_count), "#{target} expected count to be #{expected_count} for #{user_name}, got #{actual_count}"
expect(actual_count).to eq(expected_count), "#{target} expected count to be #{expected_count} for #{user_name}, got #{actual_count}: #{objects}"
end
if expected_objects
if expected_objects.empty?
expect(objects.empty?).to eq(true)
else
expect(objects).to contain_exactly(*expected_objects)
end
expect(objects).to contain_exactly(*expected_objects)
end
end
end
......
......@@ -4,10 +4,10 @@ module Gitlab
class GroupSearchResults < SearchResults
attr_reader :group
def initialize(current_user, limit_projects, group, query, default_project_filter: false)
super(current_user, limit_projects, query, default_project_filter: default_project_filter)
def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false)
@group = group
super(current_user, query, limit_projects, default_project_filter: default_project_filter)
end
# rubocop:disable CodeReuse/ActiveRecord
......
......@@ -4,11 +4,11 @@ module Gitlab
class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref
def initialize(current_user, project, query, repository_ref = nil)
@current_user = current_user
def initialize(current_user, query, project:, repository_ref: nil)
@project = project
@repository_ref = repository_ref.presence
@query = query
super(current_user, query, [project])
end
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
......
......@@ -19,10 +19,10 @@ module Gitlab
# query
attr_reader :default_project_filter
def initialize(current_user, limit_projects, query, default_project_filter: false)
def initialize(current_user, query, limit_projects = nil, default_project_filter: false)
@current_user = current_user
@limit_projects = limit_projects || Project.all
@query = query
@limit_projects = limit_projects || Project.all
@default_project_filter = default_project_filter
end
......
......@@ -4,11 +4,8 @@ module Gitlab
class SnippetSearchResults < SearchResults
include SnippetsHelper
attr_reader :current_user
def initialize(current_user, query)
@current_user = current_user
@query = query
super(current_user, query)
end
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
......
......@@ -4,9 +4,12 @@ require 'spec_helper'
RSpec.describe Gitlab::GroupSearchResults do
let(:user) { create(:user) }
let(:group) { create(:group) }
subject(:results) { described_class.new(user, 'gob', anything, group: group) }
describe 'user search' do
let(:group) { create(:group) }
subject(:objects) { results.objects('users') }
it 'returns the users belonging to the group matching the search query' do
user1 = create(:user, username: 'gob_bluth')
......@@ -17,9 +20,7 @@ RSpec.describe Gitlab::GroupSearchResults do
create(:user, username: 'gob_2018')
result = described_class.new(user, anything, group, 'gob').objects('users')
expect(result).to eq [user1]
is_expected.to eq [user1]
end
it 'returns the user belonging to the subgroup matching the search query' do
......@@ -29,9 +30,7 @@ RSpec.describe Gitlab::GroupSearchResults do
create(:user, username: 'gob_2018')
result = described_class.new(user, anything, group, 'gob').objects('users')
expect(result).to eq [user1]
is_expected.to eq [user1]
end
it 'returns the user belonging to the parent group matching the search query' do
......@@ -41,9 +40,7 @@ RSpec.describe Gitlab::GroupSearchResults do
create(:user, username: 'gob_2018')
result = described_class.new(user, anything, group, 'gob').objects('users')
expect(result).to eq [user1]
is_expected.to eq [user1]
end
it 'does not return the user belonging to the private subgroup' do
......@@ -53,9 +50,7 @@ RSpec.describe Gitlab::GroupSearchResults do
create(:user, username: 'gob_2018')
result = described_class.new(user, anything, group, 'gob').objects('users')
expect(result).to eq []
is_expected.to be_empty
end
it 'does not return the user belonging to an unrelated group' do
......@@ -63,15 +58,13 @@ RSpec.describe Gitlab::GroupSearchResults do
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 []
is_expected.to be_empty
end
end
describe "#issuable_params" do
it 'sets include_subgroups flag by default' do
result = described_class.new(user, anything, group, 'gob')
expect(result.issuable_params[:include_subgroups]).to eq(true)
expect(results.issuable_params[:include_subgroups]).to eq(true)
end
end
end
......@@ -8,28 +8,30 @@ RSpec.describe Gitlab::ProjectSearchResults do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:query) { 'hello world' }
let(:repository_ref) { nil }
describe 'initialize with empty ref' do
let(:results) { described_class.new(user, project, query, '') }
subject(:results) { described_class.new(user, query, project: project, repository_ref: repository_ref) }
it { expect(results.project).to eq(project) }
it { expect(results.query).to eq('hello world') }
end
context 'with a repository_ref' do
context 'when empty' do
let(:repository_ref) { '' }
it { expect(results.project).to eq(project) }
it { expect(results.query).to eq('hello world') }
end
describe 'initialize with ref' do
let(:ref) { 'refs/heads/test' }
let(:results) { described_class.new(user, project, query, ref) }
context 'when set' do
let(:repository_ref) { 'refs/heads/test' }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) }
it { expect(results.query).to eq('hello world') }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(repository_ref) }
it { expect(results.query).to eq('hello world') }
end
end
describe '#formatted_count' do
using RSpec::Parameterized::TableSyntax
let(:results) { described_class.new(user, project, query) }
where(:scope, :count_method, :expected) do
'blobs' | :limited_blobs_count | max_limited_count
'notes' | :limited_notes_count | max_limited_count
......@@ -63,7 +65,8 @@ RSpec.describe Gitlab::ProjectSearchResults do
shared_examples 'general blob search' do |entity_type, blob_type|
let(:query) { 'files' }
subject(:results) { described_class.new(user, project, query).objects(blob_type) }
subject(:objects) { results.objects(blob_type) }
context "when #{entity_type} is disabled" do
let(:project) { disabled_project }
......@@ -94,17 +97,17 @@ RSpec.describe Gitlab::ProjectSearchResults do
end
it 'finds by name' do
expect(results.map(&:path)).to include(expected_file_by_path)
expect(objects.map(&:path)).to include(expected_file_by_path)
end
it "loads all blobs for path matches in single batch" do
expect(Gitlab::Git::Blob).to receive(:batch).once.and_call_original
results.map(&:data)
expect { objects.map(&:data) }.not_to raise_error
end
it 'finds by content' do
blob = results.select { |result| result.path == expected_file_by_content }.flatten.last
blob = objects.select { |result| result.path == expected_file_by_content }.flatten.last
expect(blob.path).to eq(expected_file_by_content)
end
......@@ -115,7 +118,7 @@ RSpec.describe Gitlab::ProjectSearchResults do
let(:file_finder) { double }
let(:project_branch) { 'project_branch' }
subject(:results) { described_class.new(user, project, query, repository_ref).objects(blob_type) }
subject(:objects) { results.objects(blob_type) }
before do
allow(entity).to receive(:default_branch).and_return(project_branch)
......@@ -128,7 +131,7 @@ RSpec.describe Gitlab::ProjectSearchResults do
it 'uses it' do
expect(Gitlab::FileFinder).to receive(:new).with(project, repository_ref).and_return(file_finder)
results
expect { objects }.not_to raise_error
end
end
......@@ -138,7 +141,7 @@ RSpec.describe Gitlab::ProjectSearchResults do
it "uses #{entity_type} repository default reference" do
expect(Gitlab::FileFinder).to receive(:new).with(project, project_branch).and_return(file_finder)
results
expect { objects }.not_to raise_error
end
end
......@@ -148,7 +151,7 @@ RSpec.describe Gitlab::ProjectSearchResults do
it "uses #{entity_type} repository default reference" do
expect(Gitlab::FileFinder).to receive(:new).with(project, project_branch).and_return(file_finder)
results
expect { objects }.not_to raise_error
end
end
end
......@@ -157,7 +160,6 @@ RSpec.describe Gitlab::ProjectSearchResults do
let(:per_page) { 20 }
let(:count_limit) { described_class::COUNT_LIMIT }
let(:file_finder) { instance_double('Gitlab::FileFinder') }
let(:results) { described_class.new(user, project, query) }
let(:repository_ref) { 'master' }
before do
......@@ -228,139 +230,73 @@ RSpec.describe Gitlab::ProjectSearchResults do
context 'return type' do
let(:blobs) { [Gitlab::Search::FoundBlob.new(project: project)] }
let(:results) { described_class.new(user, project, "Files", per_page: 20) }
let(:query) { "Files" }
subject(:objects) { results.objects('wiki_blobs', per_page: 20) }
before do
allow(results).to receive(:wiki_blobs).and_return(blobs)
end
it 'returns list of FoundWikiPage type object' do
objects = results.objects('wiki_blobs')
expect(objects).to be_present
expect(objects).to all(be_a(Gitlab::Search::FoundWikiPage))
end
end
end
it 'does not list issues on private projects' do
issue = create(:issue, project: project)
results = described_class.new(user, project, issue.title)
expect(results.objects('issues')).not_to include issue
end
describe 'confidential issues' do
let(:query) { 'issue' }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, :internal) }
let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) }
it 'does not list project confidential issues for non project members' do
results = described_class.new(non_member, project, query)
issues = results.objects('issues')
expect(issues).to include issue
expect(issues).not_to include security_issue_1
expect(issues).not_to include security_issue_2
expect(results.limited_issues_count).to eq 1
end
it 'does not list project confidential issues for project members with guest role' do
project.add_guest(member)
results = described_class.new(member, project, query)
issues = results.objects('issues')
expect(issues).to include issue
expect(issues).not_to include security_issue_1
expect(issues).not_to include security_issue_2
expect(results.limited_issues_count).to eq 1
end
it 'lists project confidential issues for author' do
results = described_class.new(author, project, query)
issues = results.objects('issues')
expect(issues).to include issue
expect(issues).to include security_issue_1
expect(issues).not_to include security_issue_2
expect(results.limited_issues_count).to eq 2
end
it 'lists project confidential issues for assignee' do
results = described_class.new(assignee, project, query)
issues = results.objects('issues')
describe 'issues search' do
let(:issue) { create(:issue, project: project) }
let(:query) { issue.title }
expect(issues).to include issue
expect(issues).not_to include security_issue_1
expect(issues).to include security_issue_2
expect(results.limited_issues_count).to eq 2
end
it 'lists project confidential issues for project members' do
project.add_developer(member)
subject(:objects) { results.objects('issues') }
results = described_class.new(member, project, query)
issues = results.objects('issues')
expect(issues).to include issue
expect(issues).to include security_issue_1
expect(issues).to include security_issue_2
expect(results.limited_issues_count).to eq 3
it 'does not list issues on private projects' do
expect(objects).not_to include issue
end
it 'lists all project issues for admin' do
results = described_class.new(admin, project, query)
issues = results.objects('issues')
expect(issues).to include issue
expect(issues).to include security_issue_1
expect(issues).to include security_issue_2
expect(results.limited_issues_count).to eq 3
describe "confidential issues" do
include_examples "access restricted confidential issues"
end
end
describe 'notes search' do
it 'lists notes' do
project = create(:project, :public)
note = create(:note, project: project)
let(:query) { note.note }
results = described_class.new(user, project, note.note)
subject(:notes) { results.objects('notes') }
expect(results.objects('notes')).to include note
end
context 'with a public project' do
let(:project) { create(:project, :public) }
let(:note) { create(:note, project: project) }
it "doesn't list issue notes when access is restricted" do
project = create(:project, :public, :issues_private)
note = create(:note_on_issue, project: project)
it 'lists notes' do
expect(notes).to include note
end
end
results = described_class.new(user, project, note.note)
context 'with private issues' do
let(:project) { create(:project, :public, :issues_private) }
let(:note) { create(:note_on_issue, project: project) }
expect(results.objects('notes')).not_to include note
it "doesn't list issue notes when access is restricted" do
expect(notes).not_to include note
end
end
it "doesn't list merge_request notes when access is restricted" do
project = create(:project, :public, :merge_requests_private)
note = create(:note_on_merge_request, project: project)
context 'with private merge requests' do
let(:project) { create(:project, :public, :merge_requests_private) }
let(:note) { create(:note_on_merge_request, project: project) }
results = described_class.new(user, project, note.note)
expect(results.objects('notes')).not_to include note
it "doesn't list merge_request notes when access is restricted" do
expect(notes).not_to include note
end
end
end
describe '#limited_notes_count' do
let(:project) { create(:project, :public) }
let(:note) { create(:note_on_issue, project: project) }
let(:results) { described_class.new(user, project, note.note) }
let(:query) { note.note }
context 'when count_limit is lower than total amount' do
before do
......@@ -375,11 +311,6 @@ RSpec.describe Gitlab::ProjectSearchResults do
context 'when count_limit is higher than total amount' do
it 'calls note finder multiple times to get the limited amount of notes' do
project = create(:project, :public)
note = create(:note_on_issue, project: project)
results = described_class.new(user, project, note.note)
expect(results).to receive(:notes_finder).exactly(4).times.and_call_original
expect(results.limited_notes_count).to eq(1)
end
......@@ -395,7 +326,7 @@ RSpec.describe Gitlab::ProjectSearchResults do
.with(anything, anything, anything, described_class::COUNT_LIMIT)
.and_call_original
described_class.new(user, project, '.').commits_count
results.commits_count
end
end
......@@ -406,19 +337,23 @@ RSpec.describe Gitlab::ProjectSearchResults do
# * commit
#
shared_examples 'access restricted commits' do
let(:query) { search_phrase }
context 'when project is internal' do
let(:project) { create(:project, :internal, :repository) }
it 'does not search if user is not authenticated' do
commits = described_class.new(nil, project, search_phrase).objects('commits')
subject(:commits) { results.objects('commits') }
expect(commits).to be_empty
it 'searches if user is authenticated' do
expect(commits).to contain_exactly commit
end
it 'searches if user is authenticated' do
commits = described_class.new(user, project, search_phrase).objects('commits')
context 'when the user is not authenticated' do
let(:user) { nil }
expect(commits).to contain_exactly commit
it 'does not search' do
expect(commits).to be_empty
end
end
end
......@@ -437,29 +372,35 @@ RSpec.describe Gitlab::ProjectSearchResults do
user
end
it 'does not show commit to stranger' do
commits = described_class.new(nil, private_project, search_phrase).objects('commits')
let(:project) { private_project }
subject(:commits) { results.objects('commits') }
expect(commits).to be_empty
context 'when the user is not authenticated' do
let(:user) { nil }
it 'does not show commit to stranger' do
expect(commits).to be_empty
end
end
context 'team access' do
it 'shows commit to creator' do
commits = described_class.new(creator, private_project, search_phrase).objects('commits')
context 'when the user is the creator' do
let(:user) { creator }
expect(commits).to contain_exactly commit
it { expect(commits).to contain_exactly commit }
end
it 'shows commit to master' do
commits = described_class.new(team_master, private_project, search_phrase).objects('commits')
context 'when the user is a master' do
let(:user) { team_master }
expect(commits).to contain_exactly commit
it { expect(commits).to contain_exactly commit }
end
it 'shows commit to reporter' do
commits = described_class.new(team_reporter, private_project, search_phrase).objects('commits')
context 'when the user is a reporter' do
let(:user) { team_reporter }
expect(commits).to contain_exactly commit
it { expect(commits).to contain_exactly commit }
end
end
end
......@@ -471,9 +412,7 @@ RSpec.describe Gitlab::ProjectSearchResults do
it 'returns the correct results for each page' do
expect(results_page(1)).to contain_exactly(commit('b83d6e391c22777fca1ed3012fce84f633d7fed0'))
expect(results_page(2)).to contain_exactly(commit('498214de67004b1da3d820901307bed2a68a8ef6'))
expect(results_page(3)).to contain_exactly(commit('1b12f15a11fc6e62177bef08f47bc7b5ce50b141'))
end
......@@ -506,7 +445,7 @@ RSpec.describe Gitlab::ProjectSearchResults do
end
def results_page(page)
described_class.new(user, project, '.').objects('commits', per_page: 1, page: page)
described_class.new(user, '.', project: project).objects('commits', per_page: 1, page: page)
end
def commit(hash)
......@@ -518,26 +457,27 @@ RSpec.describe Gitlab::ProjectSearchResults do
let(:project) { create(:project, :public, :repository) }
let(:commit) { project.repository.commit('59e29889be61e6e0e5e223bfa9ac2721d31605b8') }
let(:message) { 'Sorry, I did a mistake' }
let(:query) { message }
it 'finds commit by message' do
commits = described_class.new(user, project, message).objects('commits')
subject(:commits) { results.objects('commits') }
it 'finds commit by message' do
expect(commits).to contain_exactly commit
end
it 'handles when no commit match' do
commits = described_class.new(user, project, 'not really an existing description').objects('commits')
context 'when there are not hits' do
let(:query) { 'not really an existing description' }
expect(commits).to be_empty
it 'handles when no commit match' do
expect(commits).to be_empty
end
end
context 'when repository_ref is provided' do
let(:message) { 'Feature added' }
let(:query) { 'Feature added' }
let(:repository_ref) { 'feature' }
it 'searches in the specified ref' do
commits = described_class.new(user, project, message, repository_ref).objects('commits')
# This commit is unique to the feature branch
expect(commits).to contain_exactly(project.repository.commit('0b4bc9a49b562e85de7cc9e834518ea6828729b9'))
end
......@@ -557,14 +497,14 @@ RSpec.describe Gitlab::ProjectSearchResults do
commit_hashes.each do |type, commit_hash|
it "shows commit by #{type} hash id" do
commits = described_class.new(user, project, commit_hash).objects('commits')
commits = described_class.new(user, commit_hash, project: project).objects('commits')
expect(commits).to contain_exactly commit
end
end
it 'handles not existing commit hash correctly' do
commits = described_class.new(user, project, 'deadbeef').objects('commits')
commits = described_class.new(user, 'deadbeef', project: project).objects('commits')
expect(commits).to be_empty
end
......@@ -577,9 +517,13 @@ RSpec.describe Gitlab::ProjectSearchResults do
end
describe 'user search' do
it 'returns the user belonging to the project matching the search query' do
project = create(:project)
let(:query) { 'gob' }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
subject(:objects) { results.objects('users') }
it 'returns the user belonging to the project matching the search query' do
user1 = create(:user, username: 'gob_bluth')
create(:project_member, :developer, user: user1, project: project)
......@@ -588,23 +532,16 @@ RSpec.describe Gitlab::ProjectSearchResults do
create(:user, username: 'gob_2018')
result = described_class.new(user, project, 'gob').objects('users')
expect(result).to eq [user1]
expect(objects).to contain_exactly(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]
expect(objects).to contain_exactly(user1)
end
end
end
......@@ -9,13 +9,10 @@ RSpec.describe Gitlab::SearchResults do
let(:user) { create(:user) }
let!(:project) { create(:project, name: 'foo') }
let!(:issue) { create(:issue, project: project, title: 'foo') }
let!(:merge_request) do
create(:merge_request, source_project: project, title: 'foo')
end
let!(:merge_request) { create(:merge_request, source_project: project, title: 'foo') }
let!(:milestone) { create(:milestone, project: project, title: 'foo') }
let(:results) { described_class.new(user, Project.all, 'foo') }
subject(:results) { described_class.new(user, 'foo', Project.all) }
context 'as a user with access' do
before do
......@@ -133,7 +130,7 @@ RSpec.describe Gitlab::SearchResults do
forked_project = fork_project(project, user)
merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo')
results = described_class.new(user, Project.where(id: forked_project.id), 'foo')
results = described_class.new(user, 'foo', Project.where(id: forked_project.id))
expect(results.objects('merge_requests')).to include merge_request_2
end
......@@ -214,7 +211,7 @@ RSpec.describe Gitlab::SearchResults do
let!(:security_issue_5) { create(:issue, :confidential, project: project_4, title: 'Security issue 5') }
it 'does not list confidential issues for non project members' do
results = described_class.new(non_member, limit_projects, query)
results = described_class.new(non_member, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include issue
......@@ -230,7 +227,7 @@ RSpec.describe Gitlab::SearchResults do
project_1.add_guest(member)
project_2.add_guest(member)
results = described_class.new(member, limit_projects, query)
results = described_class.new(member, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include issue
......@@ -243,7 +240,7 @@ RSpec.describe Gitlab::SearchResults do
end
it 'lists confidential issues for author' do
results = described_class.new(author, limit_projects, query)
results = described_class.new(author, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include issue
......@@ -256,7 +253,7 @@ RSpec.describe Gitlab::SearchResults do
end
it 'lists confidential issues for assignee' do
results = described_class.new(assignee, limit_projects, query)
results = described_class.new(assignee, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include issue
......@@ -272,7 +269,7 @@ RSpec.describe Gitlab::SearchResults do
project_1.add_developer(member)
project_2.add_developer(member)
results = described_class.new(member, limit_projects, query)
results = described_class.new(member, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include issue
......@@ -285,7 +282,7 @@ RSpec.describe Gitlab::SearchResults do
end
it 'lists all issues for admin' do
results = described_class.new(admin, limit_projects, query)
results = described_class.new(admin, query, limit_projects)
issues = results.objects('issues')
expect(issues).to include issue
......@@ -323,7 +320,7 @@ RSpec.describe Gitlab::SearchResults do
# Global search scope takes user authorized projects, internal projects and public projects.
limit_projects = ProjectsFinder.new(current_user: user).execute
milestones = described_class.new(user, limit_projects, 'milestone').objects('milestones')
milestones = described_class.new(user, 'milestone', limit_projects).objects('milestones')
expect(milestones).to match_array([milestone_1, milestone_2, milestone_3])
end
......
......@@ -261,6 +261,7 @@ RSpec.configure do |config|
./spec/support/protected_tags
./spec/support/shared_examples/features
./spec/support/shared_examples/requests
./spec/support/shared_examples/lib/gitlab
./spec/views
./spec/workers
)
......
# frozen_string_literal: true
RSpec.shared_examples 'access restricted confidential issues' do
let(:query) { 'issue' }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
let(:project) { create(:project, :internal) }
let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) }
subject(:objects) do
described_class.new(user, query, project: project).objects('issues')
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
end
end
context 'when the member is guest' do
let(:user) do
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
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
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
end
end
context 'when the user is a developper' do
let(:user) do
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
end
end
context 'when the user is admin', :request_store do
let(:user) { create(:user, admin: true) }
it 'lists all project issues' do
expect(objects).to contain_exactly(issue,
security_issue_1,
security_issue_2)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment