Commit f3baab86 authored by Michael Kozono's avatar Michael Kozono

Merge branch '12547-load-search-counts-async' into 'master'

Load search result counts asynchronously (EE)

See merge request gitlab-org/gitlab-ee!14689
parents e2be49c7 08bee854
import axios from '~/lib/utils/axios_utils';
function showCount(el, count) {
el.textContent = count;
el.classList.remove('hidden');
}
function refreshCount(el) {
const { url } = el.dataset;
return axios
.get(url)
.then(({ data }) => showCount(el, data.count))
.catch(e => {
// eslint-disable-next-line no-console
console.error(`Failed to fetch search count from '${url}'.`, e);
});
}
export default function refreshCounts() {
const elements = Array.from(document.querySelectorAll('.js-search-count'));
return Promise.all(elements.map(refreshCount));
}
......@@ -3,6 +3,7 @@ import Flash from '~/flash';
import Api from '~/api';
import { __ } from '~/locale';
import Project from '~/pages/projects/project';
import refreshCounts from './refresh_counts';
export default class Search {
constructor() {
......@@ -14,6 +15,7 @@ export default class Search {
this.groupId = $groupDropdown.data('groupId');
this.eventListeners();
refreshCounts();
$groupDropdown.glDropdown({
selectable: true,
......
......@@ -36,6 +36,15 @@ class SearchController < ApplicationController
check_single_commit_result
end
def count
params.require([:search, :scope])
scope = search_service.scope
count = search_service.search_results.formatted_count(scope)
render json: { count: count }
end
# rubocop: disable CodeReuse/ActiveRecord
def autocomplete
term = params[:term]
......
......@@ -145,17 +145,27 @@ module SearchHelper
Sanitize.clean(str)
end
def search_filter_path(options = {})
exist_opts = {
search: params[:search],
project_id: params[:project_id],
group_id: params[:group_id],
scope: params[:scope],
repository_ref: params[:repository_ref]
}
def search_filter_link(scope, label, data: {}, search: {})
search_params = params
.merge(search)
.merge({ scope: scope })
.permit(:search, :scope, :project_id, :group_id, :repository_ref, :snippets)
if @scope == scope
li_class = 'active'
count = @search_results.formatted_count(scope)
else
badge_class = 'js-search-count hidden'
badge_data = { url: search_count_path(search_params) }
end
options = exist_opts.merge(options)
search_path(options)
content_tag :li, class: li_class, data: data do
link_to search_path(search_params) do
concat label
concat ' '
concat content_tag(:span, count, class: ['badge badge-pill', badge_class], data: badge_data)
end
end
end
def search_filter_input_options(type)
......@@ -212,10 +222,6 @@ module SearchHelper
sanitize(html, tags: %w(a p ol ul li pre code))
end
def limited_count(count, limit = 1000)
count > limit ? "#{limit}+" : count
end
def search_tabs?(tab)
return false if Feature.disabled?(:users_search, default_enabled: true)
......
......@@ -438,18 +438,20 @@ class User < ApplicationRecord
order = <<~SQL
CASE
WHEN users.name = %{query} THEN 0
WHEN users.username = %{query} THEN 1
WHEN users.email = %{query} THEN 2
WHEN users.name = :query THEN 0
WHEN users.username = :query THEN 1
WHEN users.email = :query THEN 2
ELSE 3
END
SQL
sanitized_order_sql = Arel.sql(sanitize_sql_array([order, query: query]))
where(
fuzzy_arel_match(:name, query, lower_exact_match: true)
.or(fuzzy_arel_match(:username, query, lower_exact_match: true))
.or(arel_table[:email].eq(query))
).reorder(order % { query: ApplicationRecord.connection.quote(query) }, :name)
).reorder(sanitized_order_sql, :name)
end
# Limits the result set to users _not_ in the given query/list of IDs.
......
......@@ -47,5 +47,6 @@
= hidden_field_tag :snippets, true
= hidden_field_tag :repository_ref, @ref
= hidden_field_tag :nav_source, 'navbar'
-# workaround for non-JS feature specs, for JS you need to use find('#search').send_keys(:enter)
= button_tag 'Go' if ENV['RAILS_ENV'] == 'test'
.search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
- users = capture_haml do
- if search_tabs?(:members)
%li{ class: active_when(@scope == 'users') }
= link_to search_filter_path(scope: 'users') do
Users
%span.badge.badge-pill
= limited_count(@search_results.limited_users_count)
= search_filter_link 'users', _("Users")
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
......@@ -12,80 +8,28 @@
%ul.nav-links.search-filter.scrolling-tabs.nav.nav-tabs
- if @project
- if project_search_tabs?(:blobs)
%li{ class: active_when(@scope == 'blobs'), data: { qa_selector: 'code_tab' } }
= link_to search_filter_path(scope: 'blobs') do
= _("Code")
%span.badge.badge-pill
= @search_results.blobs_count
= search_filter_link 'blobs', _("Code"), data: { qa_selector: 'code_tab' }
- if project_search_tabs?(:issues)
%li{ class: active_when(@scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
= _("Issues")
%span.badge.badge-pill
= limited_count(@search_results.limited_issues_count)
= search_filter_link 'issues', _("Issues")
- if project_search_tabs?(:merge_requests)
%li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
= _("Merge requests")
%span.badge.badge-pill
= limited_count(@search_results.limited_merge_requests_count)
= search_filter_link 'merge_requests', _("Merge requests")
- if project_search_tabs?(:milestones)
%li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
= _("Milestones")
%span.badge.badge-pill
= limited_count(@search_results.limited_milestones_count)
= search_filter_link 'milestones', _("Milestones")
- if project_search_tabs?(:notes)
%li{ class: active_when(@scope == 'notes') }
= link_to search_filter_path(scope: 'notes') do
= _("Comments")
%span.badge.badge-pill
= limited_count(@search_results.limited_notes_count)
= search_filter_link 'notes', _("Comments")
- if project_search_tabs?(:wiki)
%li{ class: active_when(@scope == 'wiki_blobs') }
= link_to search_filter_path(scope: 'wiki_blobs') do
= _("Wiki")
%span.badge.badge-pill
= @search_results.wiki_blobs_count
= search_filter_link 'wiki_blobs', _("Wiki")
- if project_search_tabs?(:commits)
%li{ class: active_when(@scope == 'commits') }
= link_to search_filter_path(scope: 'commits') do
= _("Commits")
%span.badge.badge-pill
= @search_results.commits_count
= search_filter_link 'commits', _("Commits")
= users
- elsif @show_snippets
%li{ class: active_when(@scope == 'snippet_blobs') }
= link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
= _("Snippet Contents")
%span.badge.badge-pill
= @search_results.snippet_blobs_count
%li{ class: active_when(@scope == 'snippet_titles') }
= link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
= _("Titles and Filenames")
%span.badge.badge-pill
= @search_results.snippet_titles_count
= search_filter_link 'snippet_blobs', _("Snippet Contents"), search: { snippets: true, group_id: nil, project_id: nil }
= search_filter_link 'snippet_titles', _("Titles and Filenames"), search: { snippets: true, group_id: nil, project_id: nil }
- else
%li{ class: active_when(@scope == 'projects') }
= link_to search_filter_path(scope: 'projects') do
= _("Projects")
%span.badge.badge-pill
= limited_count(@search_results.limited_projects_count)
%li{ class: active_when(@scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
= _("Issues")
%span.badge.badge-pill
= limited_count(@search_results.limited_issues_count)
%li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
= _("Merge requests")
%span.badge.badge-pill
= limited_count(@search_results.limited_merge_requests_count)
%li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
= _("Milestones")
%span.badge.badge-pill
= limited_count(@search_results.limited_milestones_count)
= search_filter_link 'projects', _("Projects")
= search_filter_link 'issues', _("Issues")
= search_filter_link 'merge_requests', _("Merge requests")
= search_filter_link 'milestones', _("Milestones")
= render_if_exists 'search/category_elasticsearch'
= users
---
title: Load search result counts asynchronously
merge_request: 31663
author:
type: changed
......@@ -58,6 +58,7 @@ Rails.application.routes.draw do
# Search
get 'search' => 'search#show'
get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
get 'search/count' => 'search#count', as: :search_count
# JSON Web Token
get 'jwt/auth' => 'jwt#auth'
......
- if search_service.use_elasticsearch?
%li{ class: active_when(@scope == 'notes') }
= link_to search_filter_path(scope: 'notes') do
= _("Comments")
%span.badge.badge-pill
= limited_count(@search_results.notes_count)
%li{ class: active_when(@scope == 'blobs'), data: { qa_selector: 'code_tab' } }
= link_to search_filter_path(scope: 'blobs') do
= _("Code")
%span.badge.badge-pill
= limited_count(@search_results.blobs_count)
%li{ class: active_when(@scope == 'commits') }
= link_to search_filter_path(scope: 'commits') do
= _("Commits")
%span.badge.badge-pill
= limited_count(@search_results.commits_count)
%li{ class: active_when(@scope == 'wiki_blobs') }
= link_to search_filter_path(scope: 'wiki_blobs') do
= _("Wiki")
%span.badge.badge-pill
= limited_count(@search_results.wiki_blobs_count)
= search_filter_link 'notes', _("Comments")
= search_filter_link 'blobs', _("Code"), data: { qa_selector: 'code_tab' }
= search_filter_link 'commits', _("Commits")
= search_filter_link 'wiki_blobs', _("Wiki")
......@@ -318,7 +318,14 @@ module Elasticsearch
res
end
def delete_index_for_commits_and_blobs
def delete_index_for_commits_and_blobs(wiki: false)
types =
if wiki
%w[wiki_blob]
else
%w[commit blob]
end
client_for_indexing.delete_by_query(
index: self.class.index_name,
routing: es_parent,
......@@ -328,7 +335,7 @@ module Elasticsearch
filter: [
{
terms: {
type: %w{commit blob}
type: types
}
},
{
......
......@@ -92,7 +92,7 @@ module Gitlab
def run_indexer!(to_sha)
if index_status && !repository_contains_last_indexed_commit?
repository.delete_index_for_commits_and_blobs
repository.delete_index_for_commits_and_blobs(wiki: wiki?)
end
command =
......
......@@ -40,23 +40,6 @@ module Gitlab
@generic_search_results ||= Gitlab::ProjectSearchResults.new(current_user, project, query, repository_ref)
end
def blobs_count
@blobs_count ||= blobs.total_count
end
def notes_count
@notes_count ||= notes.total_count
end
alias_method :limited_notes_count, :notes_count
def wiki_blobs_count
@wiki_blobs_count ||= wiki_blobs.total_count
end
def commits_count
@commits_count ||= commits.total_count
end
private
def blobs
......
......@@ -53,10 +53,32 @@ module Gitlab
@generic_search_results ||= Gitlab::SearchResults.new(current_user, limit_projects, query)
end
def formatted_count(scope)
case scope
when 'projects'
projects_count.to_s
when 'notes'
notes_count.to_s
when 'blobs'
blobs_count.to_s
when 'wiki_blobs'
wiki_blobs_count.to_s
when 'commits'
commits_count.to_s
when 'issues'
issues_count.to_s
when 'merge_requests'
merge_requests_count.to_s
when 'milestones'
milestones_count.to_s
when 'users'
generic_search_results.formatted_count('users')
end
end
def projects_count
@projects_count ||= projects.total_count
end
alias_method :limited_projects_count, :projects_count
def notes_count
@notes_count ||= notes.total_count
......@@ -77,17 +99,14 @@ module Gitlab
def issues_count
@issues_count ||= issues.total_count
end
alias_method :limited_issues_count, :issues_count
def merge_requests_count
@merge_requests_count ||= merge_requests.total_count
end
alias_method :limited_merge_requests_count, :merge_requests_count
def milestones_count
@milestones_count ||= milestones.total_count
end
alias_method :limited_milestones_count, :milestones_count
def single_commit_result?
false
......
require 'spec_helper'
describe 'GlobalSearch' do
describe 'GlobalSearch', :elastic do
let(:features) { %i(issues merge_requests repository builds wiki snippets) }
let(:admin) { create :user, admin: true }
let(:auditor) {create :user, auditor: true }
......@@ -12,18 +12,12 @@ describe 'GlobalSearch' do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
project.add_developer(member)
project.add_developer(external_member)
project.add_guest(guest)
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
context "Respect feature visibility levels" do
context "Private projects" do
let(:project) { create(:project, :private, :repository, :wiki_repo) }
......
......@@ -11,10 +11,6 @@ describe 'Global elastic search', :elastic do
sign_in(user)
end
after do
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
shared_examples 'an efficient database result' do
it 'avoids N+1 database queries' do
create(object, creation_args)
......@@ -209,4 +205,28 @@ describe 'Global elastic search', :elastic do
expect(page).to have_content(expected_message)
end
end
describe 'I search globally', :js do
before do
create(:issue, project: project, title: 'project issue')
Gitlab::Elastic::Helper.refresh_index
visit dashboard_projects_path
fill_in('search', with: 'project')
find('#search').send_keys(:enter)
end
it 'displays result counts for all categories' do
expect(page).to have_content('Projects 1')
expect(page).to have_content('Issues 1')
expect(page).to have_content('Merge requests 0')
expect(page).to have_content('Milestones 0')
expect(page).to have_content('Comments 0')
expect(page).to have_content('Code 0')
expect(page).to have_content('Commits 0')
expect(page).to have_content('Wiki 0')
expect(page).to have_content('Users 0')
end
end
end
require 'spec_helper'
describe 'Group elastic search', :js do
describe 'Group elastic search', :js, :elastic do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, :wiki_repo, namespace: group) }
......@@ -16,7 +16,6 @@ describe 'Group elastic search', :js do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
project.add_maintainer(user)
group.add_owner(user)
......@@ -24,11 +23,6 @@ describe 'Group elastic search', :js do
sign_in(user)
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
describe 'issue search' do
before do
create(:issue, project: project, title: 'chosen issue title')
......
......@@ -31,7 +31,7 @@ describe Gitlab::Elastic::Indexer do
end
context 'wikis' do
let!(:project) { create(:project, :wiki_repo) }
let(:project) { create(:project, :wiki_repo) }
let(:indexer) { described_class.new(project, wiki: true) }
before do
......@@ -66,6 +66,57 @@ describe Gitlab::Elastic::Indexer do
indexer.run
end
context 'when IndexStatus#last_wiki_commit is no longer in repository', :elastic do
let(:user) { project.owner }
let(:ee_application_setting) { nil }
before do
stub_ee_application_setting(elasticsearch_indexing: true)
ElasticIndexerWorker.new.perform('index', 'Project', project.id, project.es_id)
end
def change_wiki_and_index(project, &blk)
yield blk if blk
current_commit = project.wiki.repository.commit('master').sha
described_class.new(project, wiki: true).run(current_commit)
Gitlab::Elastic::Helper.refresh_index
end
def indexed_wiki_paths_for(term)
blobs = ProjectWiki.search(
term,
type: :wiki_blob
)[:wiki_blobs][:results].response
blobs.map do |blob|
blob['_source']['blob']['path']
end
end
it 'reindexes from scratch' do
sha_for_reset = nil
change_wiki_and_index(project) do
sha_for_reset = project.wiki.repository.create_file(user, '12', '', message: '12', branch_name: 'master')
project.wiki.repository.create_file(user, '23', '', message: '23', branch_name: 'master')
end
expect(indexed_wiki_paths_for('12')).to include('12')
expect(indexed_wiki_paths_for('23')).to include('23')
project.index_status.update!(last_wiki_commit: '____________')
change_wiki_and_index(project) do
project.wiki.repository.write_ref('master', sha_for_reset)
end
expect(indexed_wiki_paths_for('12')).to include('12')
expect(indexed_wiki_paths_for('23')).not_to include('23')
end
end
end
context 'repository has unborn head' do
......@@ -250,10 +301,10 @@ describe Gitlab::Elastic::Indexer do
context 'when IndexStatus#last_commit is no longer in repository' do
before do
ElasticIndexerWorker.new.perform("index", "Project", project.id, project.es_id)
ElasticIndexerWorker.new.perform('index', 'Project', project.id, project.es_id)
end
it 'reindexes from scratch if IndexStatus#last_commit is no longer in repository' do
it 'reindexes from scratch' do
sha_for_reset = nil
change_repository_and_index(project) do
......@@ -264,7 +315,7 @@ describe Gitlab::Elastic::Indexer do
expect(indexed_file_paths_for('12')).to include('12')
expect(indexed_file_paths_for('23')).to include('23')
project.index_status.update(last_commit: '____________')
project.index_status.update!(last_commit: '____________')
change_repository_and_index(project) do
project.repository.write_ref('master', sha_for_reset)
......
require 'spec_helper'
describe Gitlab::Elastic::ProjectSearchResults do
describe Gitlab::Elastic::ProjectSearchResults, :elastic do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:query) { 'hello world' }
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
describe 'initialize with empty ref' do
......
......@@ -6,10 +6,6 @@ describe Gitlab::Elastic::SearchResults, :elastic do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end
after do
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
let(:user) { create(:user) }
let(:project_1) { create(:project, :repository, :wiki_repo) }
let(:project_2) { create(:project, :repository, :wiki_repo) }
......@@ -25,6 +21,36 @@ describe Gitlab::Elastic::SearchResults, :elastic do
end
end
describe '#formatted_count' do
using RSpec::Parameterized::TableSyntax
let(:results) { described_class.new(user, 'hello world', limit_project_ids) }
where(:scope, :count_method, :expected) do
'projects' | :projects_count | '1234'
'notes' | :notes_count | '1234'
'blobs' | :blobs_count | '1234'
'wiki_blobs' | :wiki_blobs_count | '1234'
'commits' | :commits_count | '1234'
'issues' | :issues_count | '1234'
'merge_requests' | :merge_requests_count | '1234'
'milestones' | :milestones_count | '1234'
'unknown' | nil | nil
end
with_them do
it 'returns the expected formatted count' do
expect(results).to receive(count_method).and_return(1234) if count_method
expect(results.formatted_count(scope)).to eq(expected)
end
end
it 'delegates to generic_search_results for users' do
expect(results.generic_search_results).to receive(:formatted_count).with('users').and_return('1000+')
expect(results.formatted_count('users')).to eq('1000+')
end
end
shared_examples_for 'a paginated object' do |object_type|
let(:results) { described_class.new(user, 'hello world', limit_project_ids) }
......
......@@ -36,12 +36,6 @@ describe API::Search do
shared_examples 'elasticsearch enabled' do
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end
context 'for wiki_blobs scope' do
......@@ -119,7 +113,7 @@ describe API::Search do
end
end
context 'when elasticsearch is enabled' do
context 'when elasticsearch is enabled', :elastic do
it_behaves_like 'elasticsearch enabled' do
let(:endpoint) { '/search' }
end
......@@ -135,7 +129,7 @@ describe API::Search do
end
end
context 'when elasticsearch is enabled' do
context 'when elasticsearch is enabled', :elastic do
it_behaves_like 'elasticsearch enabled' do
let(:endpoint) { "/groups/#{group.id}/-/search" }
end
......
......@@ -29,6 +29,21 @@ module Gitlab
end
end
def formatted_count(scope)
case scope
when 'blobs'
blobs_count.to_s
when 'notes'
formatted_limited_count(limited_notes_count)
when 'wiki_blobs'
wiki_blobs_count.to_s
when 'commits'
commits_count.to_s
else
super
end
end
def users
super.where(id: @project.team.members) # rubocop:disable CodeReuse/ActiveRecord
end
......
......@@ -43,6 +43,29 @@ module Gitlab
without_count ? collection.without_count : collection
end
def formatted_count(scope)
case scope
when 'projects'
formatted_limited_count(limited_projects_count)
when 'issues'
formatted_limited_count(limited_issues_count)
when 'merge_requests'
formatted_limited_count(limited_merge_requests_count)
when 'milestones'
formatted_limited_count(limited_milestones_count)
when 'users'
formatted_limited_count(limited_users_count)
end
end
def formatted_limited_count(count)
if count >= COUNT_LIMIT
"#{COUNT_LIMIT - 1}+"
else
count.to_s
end
end
def limited_projects_count
@limited_projects_count ||= limited_count(projects)
end
......
......@@ -22,6 +22,17 @@ module Gitlab
end
end
def formatted_count(scope)
case scope
when 'snippet_titles'
snippet_titles_count.to_s
when 'snippet_blobs'
snippet_blobs_count.to_s
else
super
end
end
def snippet_titles_count
@snippet_titles_count ||= snippet_titles.count
end
......
......@@ -11,151 +11,173 @@ describe SearchController do
sign_in(user)
end
context 'uses the right partials depending on scope' do
using RSpec::Parameterized::TableSyntax
render_views
set(:project) { create(:project, :public, :repository, :wiki_repo) }
shared_examples_for 'when the user cannot read cross project' do |action, params|
before do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
.with(user, :read_cross_project, :global) { false }
end
subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
it 'blocks access without a project_id' do
get action, params: params
where(:partial, :scope) do
'_blob' | :blobs
'_wiki_blob' | :wiki_blobs
'_commit' | :commits
expect(response).to have_gitlab_http_status(403)
end
with_them do
it do
project_wiki = create(:project_wiki, project: project, user: user)
create(:wiki_page, wiki: project_wiki, attrs: { title: 'merge', content: 'merge' })
it 'allows access with a project_id' do
get action, params: params.merge(project_id: create(:project, :public).id)
expect(subject).to render_template("search/results/#{partial}")
end
expect(response).to have_gitlab_http_status(200)
end
end
context 'global search' do
render_views
it 'omits pipeline status from load' do
project = create(:project, :public)
expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects)
get :show, params: { scope: 'projects', search: project.name }
shared_examples_for 'with external authorization service enabled' do |action, params|
let(:project) { create(:project, namespace: user.namespace) }
let(:note) { create(:note_on_issue, project: project) }
expect(assigns[:search_objects].first).to eq project
before do
enable_external_authorization_service_check
end
end
it 'finds issue comments' do
project = create(:project, :public)
note = create(:note_on_issue, project: project)
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
it 'renders a 403 when no project is given' do
get action, params: params
expect(assigns[:search_objects].first).to eq note
end
context 'when the user cannot read cross project' do
before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
.with(user, :read_cross_project, :global) { false }
expect(response).to have_gitlab_http_status(403)
end
it 'still allows accessing the search page' do
get :show
it 'renders a 200 when a project was set' do
get action, params: params.merge(project_id: project.id)
expect(response).to have_gitlab_http_status(200)
end
end
it 'still blocks searches without a project_id' do
get :show, params: { search: 'hello' }
describe 'GET #show' do
it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do
it 'still allows accessing the search page' do
get :show
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(200)
end
end
it 'allows searches with a project_id' do
get :show, params: { search: 'hello', project_id: create(:project, :public).id }
it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' }
expect(response).to have_gitlab_http_status(200)
end
end
context 'uses the right partials depending on scope' do
using RSpec::Parameterized::TableSyntax
render_views
set(:project) { create(:project, :public, :repository, :wiki_repo) }
context 'on restricted projects' do
context 'when signed out' do
before do
sign_out(user)
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
end
it "doesn't expose comments on issues" do
project = create(:project, :public, :issues_private)
note = create(:note_on_issue, project: project)
subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
where(:partial, :scope) do
'_blob' | :blobs
'_wiki_blob' | :wiki_blobs
'_commit' | :commits
end
expect(assigns[:search_objects].count).to eq(0)
with_them do
it do
project_wiki = create(:project_wiki, project: project, user: user)
create(:wiki_page, wiki: project_wiki, attrs: { title: 'merge', content: 'merge' })
expect(subject).to render_template("search/results/#{partial}")
end
end
end
it "doesn't expose comments on merge_requests" do
project = create(:project, :public, :merge_requests_private)
note = create(:note_on_merge_request, project: project)
context 'global search' do
render_views
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
it 'omits pipeline status from load' do
project = create(:project, :public)
expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects)
get :show, params: { scope: 'projects', search: project.name }
expect(assigns[:search_objects].count).to eq(0)
expect(assigns[:search_objects].first).to eq project
end
end
it "doesn't expose comments on snippets" do
project = create(:project, :public, :snippets_private)
note = create(:note_on_project_snippet, project: project)
it 'finds issue comments' do
project = create(:project, :public)
note = create(:note_on_issue, project: project)
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
expect(assigns[:search_objects].count).to eq(0)
expect(assigns[:search_objects].first).to eq note
end
end
context 'with external authorization service enabled' do
let(:project) { create(:project, namespace: user.namespace) }
let(:note) { create(:note_on_issue, project: project) }
context 'on restricted projects' do
context 'when signed out' do
before do
sign_out(user)
end
before do
enable_external_authorization_service_check
end
it "doesn't expose comments on issues" do
project = create(:project, :public, :issues_private)
note = create(:note_on_issue, project: project)
describe 'GET #show' do
it 'renders a 403 when no project is given' do
get :show, params: { scope: 'notes', search: note.note }
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
expect(response).to have_gitlab_http_status(403)
expect(assigns[:search_objects].count).to eq(0)
end
end
it 'renders a 200 when a project was set' do
it "doesn't expose comments on merge_requests" do
project = create(:project, :public, :merge_requests_private)
note = create(:note_on_merge_request, project: project)
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
expect(response).to have_gitlab_http_status(200)
expect(assigns[:search_objects].count).to eq(0)
end
end
describe 'GET #autocomplete' do
it 'renders a 403 when no project is given' do
get :autocomplete, params: { term: 'hello' }
it "doesn't expose comments on snippets" do
project = create(:project, :public, :snippets_private)
note = create(:note_on_project_snippet, project: project)
expect(response).to have_gitlab_http_status(403)
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
expect(assigns[:search_objects].count).to eq(0)
end
end
end
it 'renders a 200 when a project was set' do
get :autocomplete, params: { project_id: project.id, term: 'hello' }
describe 'GET #count' do
it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' }
it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' }
expect(response).to have_gitlab_http_status(200)
end
it 'returns the result count for the given term and scope' do
create(:project, :public, name: 'hello world')
create(:project, :public, name: 'foo bar')
get :count, params: { search: 'hello', scope: 'projects' }
expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq({ 'count' => '1' })
end
it 'raises an error if search term is missing' do
expect do
get :count, params: { scope: 'projects' }
end.to raise_error(ActionController::ParameterMissing)
end
it 'raises an error if search scope is missing' do
expect do
get :count, params: { search: 'hello' }
end.to raise_error(ActionController::ParameterMissing)
end
end
describe 'GET #autocomplete' do
it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' }
it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' }
end
end
......@@ -4,7 +4,7 @@ require 'spec_helper'
describe 'User searches for users' do
context 'when on the dashboard' do
it 'finds the user' do
it 'finds the user', :js do
create(:user, username: 'gob_bluth', name: 'Gob Bluth')
sign_in(create(:user))
......@@ -12,7 +12,7 @@ describe 'User searches for users' do
visit dashboard_projects_path
fill_in 'search', with: 'gob'
click_button 'Go'
find('#search').send_keys(:enter)
expect(page).to have_content('Users 1')
......
......@@ -96,6 +96,23 @@ describe 'User uses header search field', :js do
let(:url) { root_path }
let(:scope_name) { 'All GitLab' }
end
context 'when searching through the search field' do
before do
create(:issue, project: project, title: 'project issue')
fill_in('search', with: 'project')
find('#search').send_keys(:enter)
end
it 'displays result counts for all categories' do
expect(page).to have_content('Projects 1')
expect(page).to have_content('Issues 1')
expect(page).to have_content('Merge requests 0')
expect(page).to have_content('Milestones 0')
expect(page).to have_content('Users 0')
end
end
end
context 'when user is in a project scope' do
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`pages/search/show/refresh_counts fetches and displays search counts 1`] = `
"<div class=\\"badge\\">22</div>
<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&amp;project_id=3&amp;scope=issues\\">4</div>
<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&amp;project_id=3&amp;scope=merge_requests\\">5</div>"
`;
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import refreshCounts from '~/pages/search/show/refresh_counts';
const URL = `${TEST_HOST}/search/count?search=lorem+ipsum&project_id=3`;
const urlWithScope = scope => `${URL}&scope=${scope}`;
const counts = [{ scope: 'issues', count: 4 }, { scope: 'merge_requests', count: 5 }];
const fixture = `<div class="badge">22</div>
<div class="badge js-search-count hidden" data-url="${urlWithScope('issues')}"></div>
<div class="badge js-search-count hidden" data-url="${urlWithScope('merge_requests')}"></div>`;
describe('pages/search/show/refresh_counts', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
setFixtures(fixture);
});
afterEach(() => {
mock.restore();
});
it('fetches and displays search counts', () => {
counts.forEach(({ scope, count }) => {
mock.onGet(urlWithScope(scope)).reply(200, { count });
});
// assert before act behavior
return refreshCounts().then(() => {
expect(document.body.innerHTML).toMatchSnapshot();
});
});
});
......@@ -177,4 +177,48 @@ describe SearchHelper do
end
end
end
describe 'search_filter_link' do
it 'renders a search filter link for the current scope' do
@scope = 'projects'
@search_results = double
expect(@search_results).to receive(:formatted_count).with('projects').and_return('23')
link = search_filter_link('projects', 'Projects')
expect(link).to have_css('li.active')
expect(link).to have_link('Projects', href: search_path(scope: 'projects'))
expect(link).to have_css('span.badge.badge-pill:not(.js-search-count):not(.hidden):not([data-url])', text: '23')
end
it 'renders a search filter link for another scope' do
link = search_filter_link('projects', 'Projects')
count_path = search_count_path(scope: 'projects')
expect(link).to have_css('li:not([class="active"])')
expect(link).to have_link('Projects', href: search_path(scope: 'projects'))
expect(link).to have_css("span.badge.badge-pill.js-search-count.hidden[data-url='#{count_path}']", text: '')
end
it 'merges in the current search params and given params' do
expect(self).to receive(:params).and_return(
ActionController::Parameters.new(
search: 'hello',
scope: 'ignored',
other_param: 'ignored'
)
)
link = search_filter_link('projects', 'Projects', search: { project_id: 23 })
expect(link).to have_link('Projects', href: search_path(scope: 'projects', search: 'hello', project_id: 23))
end
it 'assigns given data attributes on the list container' do
link = search_filter_link('projects', 'Projects', data: { foo: 'bar' })
expect(link).to have_css('li[data-foo="bar"]')
end
end
end
......@@ -22,6 +22,28 @@ describe Gitlab::ProjectSearchResults do
it { expect(results.query).to eq('hello world') }
end
describe '#formatted_count' do
using RSpec::Parameterized::TableSyntax
let(:results) { described_class.new(user, project, query) }
where(:scope, :count_method, :expected) do
'blobs' | :blobs_count | '1234'
'notes' | :limited_notes_count | '1000+'
'wiki_blobs' | :wiki_blobs_count | '1234'
'commits' | :commits_count | '1234'
'projects' | :limited_projects_count | '1000+'
'unknown' | nil | nil
end
with_them do
it 'returns the expected formatted count' do
expect(results).to receive(count_method).and_return(1234) if count_method
expect(results.formatted_count(scope)).to eq(expected)
end
end
end
shared_examples 'general blob search' do |entity_type, blob_kind|
let(:query) { 'files' }
subject(:results) { described_class.new(user, project, query).objects(blob_type) }
......
......@@ -29,6 +29,43 @@ describe Gitlab::SearchResults do
end
end
describe '#formatted_count' do
using RSpec::Parameterized::TableSyntax
where(:scope, :count_method, :expected) do
'projects' | :limited_projects_count | '1000+'
'issues' | :limited_issues_count | '1000+'
'merge_requests' | :limited_merge_requests_count | '1000+'
'milestones' | :limited_milestones_count | '1000+'
'users' | :limited_users_count | '1000+'
'unknown' | nil | nil
end
with_them do
it 'returns the expected formatted count' do
expect(results).to receive(count_method).and_return(1234) if count_method
expect(results.formatted_count(scope)).to eq(expected)
end
end
end
describe '#formatted_limited_count' do
using RSpec::Parameterized::TableSyntax
where(:count, :expected) do
23 | '23'
1000 | '1000'
1001 | '1000+'
1234 | '1000+'
end
with_them do
it 'returns the expected formatted limited count' do
expect(results.formatted_limited_count(count)).to eq(expected)
end
end
end
context "when count_limit is lower than total amount" do
before do
allow(results).to receive(:count_limit).and_return(1)
......
......@@ -16,4 +16,22 @@ describe Gitlab::SnippetSearchResults do
expect(results.snippet_blobs_count).to eq(1)
end
end
describe '#formatted_count' do
using RSpec::Parameterized::TableSyntax
where(:scope, :count_method, :expected) do
'snippet_titles' | :snippet_titles_count | '1234'
'snippet_blobs' | :snippet_blobs_count | '1234'
'projects' | :limited_projects_count | '1000+'
'unknown' | nil | nil
end
with_them do
it 'returns the expected formatted count' do
expect(results).to receive(count_method).and_return(1234) if count_method
expect(results.formatted_count(scope)).to eq(expected)
end
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