diff options
author | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2018-08-20 15:11:03 +0000 |
---|---|---|
committer | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2018-08-20 15:11:03 +0000 |
commit | b289075d7b69282f8d863b59278f9e03a4f9a5e1 (patch) | |
tree | d7e7eccd18d15de103a7411d82de3e9e47497e5b | |
parent | 64d4f3b2a45a01e23f6c45515f4f0271ea6fd4a0 (diff) | |
parent | 6f3c490107f5fa7dd00bce0bbd89e4a0fa4d6389 (diff) | |
download | gitlab-ce-b289075d7b69282f8d863b59278f9e03a4f9a5e1.tar.gz |
Merge branch 'define-abstraction-levels' into 'master'
Refactor AutocompleteController
See merge request gitlab-org/gitlab-ce!20908
32 files changed, 879 insertions, 189 deletions
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 86bade49ec9..9e30b982b06 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -1,67 +1,39 @@ class AutocompleteController < ApplicationController - AWARD_EMOJI_MAX = 100 - skip_before_action :authenticate_user!, only: [:users, :award_emojis] - before_action :load_project, only: [:users] - before_action :load_group, only: [:users] def users - @users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute - - render json: UserSerializer.new.represent(@users) - end - - def user - @user = User.find(params[:id]) - render json: UserSerializer.new.represent(@user) - end - - def projects - project = Project.find_by_id(params[:project_id]) - projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id]) + project = Autocomplete::ProjectFinder + .new(current_user, params) + .execute - render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace) - end + group = Autocomplete::GroupFinder + .new(current_user, project, params) + .execute - def award_emojis - emoji_with_count = AwardEmoji - .limit(AWARD_EMOJI_MAX) - .where(user: current_user) - .group(:name) - .order('count_all DESC, name ASC') - .count + users = Autocomplete::UsersFinder + .new(params: params, current_user: current_user, project: project, group: group) + .execute - # Transform from hash to array to guarantee json order - # e.g. { 'thumbsup' => 2, 'thumbsdown' = 1 } - # => [{ name: 'thumbsup' }, { name: 'thumbsdown' }] - render json: emoji_with_count.map { |k, v| { name: k } } + render json: UserSerializer.new.represent(users) end - private - - def load_group - @group ||= begin - if @project.blank? && params[:group_id].present? - group = Group.find(params[:group_id]) - return render_404 unless can?(current_user, :read_group, group) + def user + user = UserFinder.new(params).execute! - group - end - end + render json: UserSerializer.new.represent(user) end - def load_project - @project ||= begin - if params[:project_id].present? - project = Project.find(params[:project_id]) - return render_404 unless can?(current_user, :read_project, project) + # Displays projects to use for the dropdown when moving a resource from one + # project to another. + def projects + projects = Autocomplete::MoveToProjectFinder + .new(current_user, params) + .execute - project - end - end + render json: MoveToProjectSerializer.new.represent(projects) end - def projects_finder - MoveToProjectFinder.new(current_user) + def award_emojis + render json: AwardedEmojiFinder.new(current_user).execute end end diff --git a/app/finders/autocomplete/group_finder.rb b/app/finders/autocomplete/group_finder.rb new file mode 100644 index 00000000000..dd97ac4c817 --- /dev/null +++ b/app/finders/autocomplete/group_finder.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Autocomplete + # Finder for retrieving a group to use for autocomplete data sources. + class GroupFinder + attr_reader :current_user, :project, :group_id + + # current_user - The currently logged in user, if any. + # project - The Project (if any) to use for the autocomplete data sources. + # params - A Hash containing parameters to use for finding the project. + # + # The following parameters are supported: + # + # * group_id: The ID of the group to find. + def initialize(current_user = nil, project = nil, params = {}) + @current_user = current_user + @project = project + @group_id = params[:group_id] + end + + # Attempts to find a Group based on the current group ID. + def execute + return unless project.blank? && group_id.present? + + group = Group.find(group_id) + + # This removes the need for using `return render_404` and similar patterns + # in controllers that use this finder. + unless Ability.allowed?(current_user, :read_group, group) + raise ActiveRecord::RecordNotFound + .new("Could not find a Group with ID #{group_id}") + end + + group + end + end +end diff --git a/app/finders/autocomplete/move_to_project_finder.rb b/app/finders/autocomplete/move_to_project_finder.rb new file mode 100644 index 00000000000..edaf74c5f92 --- /dev/null +++ b/app/finders/autocomplete/move_to_project_finder.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Autocomplete + # Finder that retrieves a list of projects that an issue can be moved to. + class MoveToProjectFinder + attr_reader :current_user, :search, :project_id, :offset_id + + # current_user - The User object of the user that wants to view the list of + # projects. + # + # params - A Hash containing additional parameters to set. + # + # The following parameters can be set (as Symbols): + # + # * search: An optional search query to apply to the list of projects. + # * project_id: The ID of a project to exclude from the returned relation. + # * offset_id: The ID of a project to use for pagination. When given, only + # projects with a lower ID are included in the list. + def initialize(current_user, params = {}) + @current_user = current_user + @search = params[:search] + @project_id = params[:project_id] + @offset_id = params[:offset_id] + end + + def execute + current_user + .projects_where_can_admin_issues + .optionally_search(search) + .excluding_project(project_id) + .paginate_in_descending_order_using_id(before: offset_id) + .eager_load_namespace_and_owner + end + end +end diff --git a/app/finders/autocomplete/project_finder.rb b/app/finders/autocomplete/project_finder.rb new file mode 100644 index 00000000000..3a4696f4c2e --- /dev/null +++ b/app/finders/autocomplete/project_finder.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Autocomplete + # Finder for retrieving a project to use for autocomplete data sources. + class ProjectFinder + attr_reader :current_user, :project_id + + # current_user - The currently logged in user, if any. + # params - A Hash containing parameters to use for finding the project. + # + # The following parameters are supported: + # + # * project_id: The ID of the project to find. + def initialize(current_user = nil, params = {}) + @current_user = current_user + @project_id = params[:project_id] + end + + # Attempts to find a Project based on the current project ID. + def execute + return if project_id.blank? + + project = Project.find(project_id) + + # This removes the need for using `return render_404` and similar patterns + # in controllers that use this finder. + unless Ability.allowed?(current_user, :read_project, project) + raise ActiveRecord::RecordNotFound + .new("Could not find a Project with ID #{project_id}") + end + + project + end + end +end diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb new file mode 100644 index 00000000000..b2557469079 --- /dev/null +++ b/app/finders/autocomplete/users_finder.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module Autocomplete + class UsersFinder + # The number of users to display in the results is hardcoded to 20, and + # pagination is not supported. This ensures that performance remains + # consistent and removes the need for implementing keyset pagination to + # ensure good performance. + LIMIT = 20 + + attr_reader :current_user, :project, :group, :search, :skip_users, + :author_id, :todo_filter, :todo_state_filter, + :filter_by_current_user + + def initialize(params:, current_user:, project:, group:) + @current_user = current_user + @project = project + @group = group + @search = params[:search] + @skip_users = params[:skip_users] + @author_id = params[:author_id] + @todo_filter = params[:todo_filter] + @todo_state_filter = params[:todo_state_filter] + @filter_by_current_user = params[:current_user] + end + + def execute + items = limited_users + + if search.blank? + # Include current user if available to filter by "Me" + items.unshift(current_user) if prepend_current_user? + + if prepend_author? && (author = User.find_by_id(author_id)) + items.unshift(author) + end + end + + items.uniq + end + + private + + # Returns the users based on the input parameters, as an Array. + # + # This method is separate so it is easier to extend in EE. + def limited_users + # When changing the order of these method calls, make sure that + # reorder_by_name() is called _before_ optionally_search(), otherwise + # reorder_by_name will break the ORDER BY applied in optionally_search(). + find_users + .active + .reorder_by_name + .optionally_search(search) + .where_not_in(skip_users) + .limit_to_todo_authors( + user: current_user, + with_todos: todo_filter, + todo_state: todo_state_filter + ) + .limit(LIMIT) + .to_a + end + + def prepend_current_user? + filter_by_current_user.present? && current_user + end + + def prepend_author? + author_id.present? && current_user + end + + def find_users + if project + project.authorized_users.union_with_user(author_id) + elsif group + group.users_with_parents + elsif current_user + User.all + else + User.none + end + end + end +end diff --git a/app/finders/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb deleted file mode 100644 index e8a03947f59..00000000000 --- a/app/finders/autocomplete_users_finder.rb +++ /dev/null @@ -1,68 +0,0 @@ -class AutocompleteUsersFinder - # The number of users to display in the results is hardcoded to 20, and - # pagination is not supported. This ensures that performance remains - # consistent and removes the need for implementing keyset pagination to ensure - # good performance. - LIMIT = 20 - - attr_reader :current_user, :project, :group, :search, :skip_users, - :author_id, :params - - def initialize(params:, current_user:, project:, group:) - @current_user = current_user - @project = project - @group = group - @search = params[:search] - @skip_users = params[:skip_users] - @author_id = params[:author_id] - @params = params - end - - def execute - items = find_users - items = items.active - items = items.reorder(:name) - items = items.search(search) if search.present? - items = items.where.not(id: skip_users) if skip_users.present? - items = items.limit(LIMIT) - - if params[:todo_filter].present? && current_user - items = items.todo_authors(current_user.id, params[:todo_state_filter]) - end - - if search.blank? - # Include current user if available to filter by "Me" - if params[:current_user].present? && current_user - items = [current_user, *items].uniq - end - - if author_id.present? && current_user - author = User.find_by_id(author_id) - items = [author, *items].uniq if author - end - end - - items - end - - private - - def find_users - return users_from_project if project - return group.users_with_parents if group - return User.all if current_user - - User.none - end - - def users_from_project - if author_id.present? - union = Gitlab::SQL::Union - .new([project.authorized_users, User.where(id: author_id)]) - - User.from("(#{union.to_sql}) #{User.table_name}") - else - project.authorized_users - end - end -end diff --git a/app/finders/awarded_emoji_finder.rb b/app/finders/awarded_emoji_finder.rb new file mode 100644 index 00000000000..f0cc17f3b26 --- /dev/null +++ b/app/finders/awarded_emoji_finder.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Class for retrieving information about emoji awarded _by_ a particular user. +class AwardedEmojiFinder + attr_reader :current_user + + # current_user - The User to generate the data for. + def initialize(current_user = nil) + @current_user = current_user + end + + def execute + return [] unless current_user + + # We want the resulting data set to be an Array containing the emoji names + # in descending order, based on how often they were awarded. + AwardEmoji + .award_counts_for_user(current_user) + .map { |name, _| { name: name } } + end +end diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb deleted file mode 100644 index 038d5565a1e..00000000000 --- a/app/finders/move_to_project_finder.rb +++ /dev/null @@ -1,21 +0,0 @@ -class MoveToProjectFinder - PAGE_SIZE = 50 - - def initialize(user) - @user = user - end - - def execute(from_project, search: nil, offset_id: nil) - projects = @user.projects_where_can_admin_issues - projects = projects.search(search) if search.present? - projects = projects.excluding_project(from_project) - projects = projects.order_id_desc - - # infinite scroll using offset - projects = projects.where('projects.id < ?', offset_id) if offset_id.present? - projects = projects.limit(PAGE_SIZE) - - # to ask for Project#name_with_namespace - projects.includes(namespace: :owner) - end -end diff --git a/app/finders/user_finder.rb b/app/finders/user_finder.rb new file mode 100644 index 00000000000..484a93c9873 --- /dev/null +++ b/app/finders/user_finder.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# A simple finding for obtaining a single User. +# +# While using `User.find_by` directly is straightforward, it can lead to a lot +# of code duplication. Sometimes we just want to find a user by an ID, other +# times we may want to exclude blocked user. By using this finder (and extending +# it whenever necessary) we can keep this logic in one place. +class UserFinder + attr_reader :params + + def initialize(params) + @params = params + end + + # Tries to find a User, returning nil if none could be found. + def execute + User.find_by(id: params[:id]) + end + + # Tries to find a User, raising a `ActiveRecord::RecordNotFound` if it could + # not be found. + def execute! + User.find(params[:id]) + end +end diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 99c7866d636..ddc516ccb60 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -28,6 +28,23 @@ class AwardEmoji < ActiveRecord::Base .where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids) .group('name', 'awardable_id') end + + # Returns the top 100 emoji awarded by the given user. + # + # The returned value is a Hash mapping emoji names to the number of times + # they were awarded: + # + # { 'thumbsup' => 2, 'thumbsdown' => 1 } + # + # user - The User to get the awards for. + # limt - The maximum number of emoji to return. + def award_counts_for_user(user, limit = 100) + limit(limit) + .where(user: user) + .group(:name) + .order('count_all DESC, name ASC') + .count + end end def downvote? diff --git a/app/models/concerns/optionally_search.rb b/app/models/concerns/optionally_search.rb new file mode 100644 index 00000000000..dec97b7dee8 --- /dev/null +++ b/app/models/concerns/optionally_search.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module OptionallySearch + extend ActiveSupport::Concern + + module ClassMethods + def search(*) + raise( + NotImplementedError, + 'Your model must implement the "search" class method' + ) + end + + # Optionally limits a result set to those matching the given search query. + def optionally_search(query = nil) + query.present? ? search(query) : all + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 94c1d60f071..15336ec2ea2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -28,6 +28,7 @@ class Project < ActiveRecord::Base include WithUploads include BatchDestroyDependentAssociations include FeatureGate + include OptionallySearch extend Gitlab::Cache::RequestCache extend Gitlab::ConfigHelper @@ -384,6 +385,26 @@ class Project < ActiveRecord::Base only_integer: true, message: 'needs to be beetween 10 minutes and 1 month' } + # Paginates a collection using a `WHERE id < ?` condition. + # + # before - A project ID to use for filtering out projects with an equal or + # greater ID. If no ID is given, all projects are included. + # + # limit - The maximum number of rows to include. + def self.paginate_in_descending_order_using_id( + before: nil, + limit: Kaminari.config.default_per_page + ) + relation = order_id_desc.limit(limit) + relation = relation.where('projects.id < ?', before) if before + + relation + end + + def self.eager_load_namespace_and_owner + includes(namespace: :owner) + end + # Returns a collection of projects that is either public or visible to the # logged in user. def self.public_or_visible_to_user(user = nil) diff --git a/app/models/user.rb b/app/models/user.rb index 13b04270a4a..a6ba90794d6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,6 +19,7 @@ class User < ActiveRecord::Base include BulkMemberAccessLoad include BlocksJsonSerialization include WithUploads + include OptionallySearch DEFAULT_NOTIFICATION_LEVEL = :participating @@ -253,11 +254,41 @@ class User < ActiveRecord::Base scope :external, -> { where(external: true) } scope :active, -> { with_state(:active).non_internal } scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) } - scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) } scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } scope :confirmed, -> { where.not(confirmed_at: nil) } + # Limits the users to those that have TODOs, optionally in the given state. + # + # user - The user to get the todos for. + # + # with_todos - If we should limit the result set to users that are the + # authors of todos. + # + # todo_state - An optional state to require the todos to be in. + def self.limit_to_todo_authors(user: nil, with_todos: false, todo_state: nil) + if user && with_todos + where(id: Todo.where(user: user, state: todo_state).select(:author_id)) + else + all + end + end + + # Returns a relation that optionally includes the given user. + # + # user_id - The ID of the user to include. + def self.union_with_user(user_id = nil) + if user_id.present? + union = Gitlab::SQL::Union.new([all, User.unscoped.where(id: user_id)]) + + # We use "unscoped" here so that any inner conditions are not repeated for + # the outer query, which would be redundant. + User.unscoped.from("(#{union.to_sql}) #{User.table_name}") + else + all + end + end + def self.with_two_factor_indistinct joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") .where("u2f.id IS NOT NULL OR users.otp_required_for_login = ?", true) @@ -365,6 +396,18 @@ class User < ActiveRecord::Base ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name) end + # Limits the result set to users _not_ in the given query/list of IDs. + # + # users - The list of users to ignore. This can be an + # `ActiveRecord::Relation`, or an Array. + def where_not_in(users = nil) + users ? where.not(id: users) : all + end + + def reorder_by_name + reorder(:name) + end + # searches user by given pattern # it compares name, email, username fields and user's secondary emails with given pattern # This method uses ILIKE on PostgreSQL and LIKE on MySQL. diff --git a/app/serializers/move_to_project_entity.rb b/app/serializers/move_to_project_entity.rb new file mode 100644 index 00000000000..dac1124b0b3 --- /dev/null +++ b/app/serializers/move_to_project_entity.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class MoveToProjectEntity < Grape::Entity + expose :id + expose :name_with_namespace +end diff --git a/app/serializers/move_to_project_serializer.rb b/app/serializers/move_to_project_serializer.rb new file mode 100644 index 00000000000..6a59317505c --- /dev/null +++ b/app/serializers/move_to_project_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class MoveToProjectSerializer < BaseSerializer + entity MoveToProjectEntity +end diff --git a/lib/gitlab/github_import/importer/diff_note_importer.rb b/lib/gitlab/github_import/importer/diff_note_importer.rb index 8274f37d358..d562958e955 100644 --- a/lib/gitlab/github_import/importer/diff_note_importer.rb +++ b/lib/gitlab/github_import/importer/diff_note_importer.rb @@ -13,7 +13,7 @@ module Gitlab @note = note @project = project @client = client - @user_finder = UserFinder.new(project, client) + @user_finder = GithubImport::UserFinder.new(project, client) end def execute diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb index ead4215810f..cb4d7a6a0b6 100644 --- a/lib/gitlab/github_import/importer/issue_importer.rb +++ b/lib/gitlab/github_import/importer/issue_importer.rb @@ -19,7 +19,7 @@ module Gitlab @issue = issue @project = project @client = client - @user_finder = UserFinder.new(project, client) + @user_finder = GithubImport::UserFinder.new(project, client) @milestone_finder = MilestoneFinder.new(project) @issuable_finder = GithubImport::IssuableFinder.new(project, issue) end diff --git a/lib/gitlab/github_import/importer/note_importer.rb b/lib/gitlab/github_import/importer/note_importer.rb index c890f2df360..2b06d1b3baf 100644 --- a/lib/gitlab/github_import/importer/note_importer.rb +++ b/lib/gitlab/github_import/importer/note_importer.rb @@ -13,7 +13,7 @@ module Gitlab @note = note @project = project @client = client - @user_finder = UserFinder.new(project, client) + @user_finder = GithubImport::UserFinder.new(project, client) end def execute diff --git a/lib/gitlab/github_import/importer/pull_request_importer.rb b/lib/gitlab/github_import/importer/pull_request_importer.rb index e4b49d2143a..ed17aa54373 100644 --- a/lib/gitlab/github_import/importer/pull_request_importer.rb +++ b/lib/gitlab/github_import/importer/pull_request_importer.rb @@ -15,7 +15,7 @@ module Gitlab @pull_request = pull_request @project = project @client = client - @user_finder = UserFinder.new(project, client) + @user_finder = GithubImport::UserFinder.new(project, client) @milestone_finder = MilestoneFinder.new(project) @issuable_finder = GithubImport::IssuableFinder.new(project, pull_request) diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 2c59d1929a1..883bb35f396 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -274,14 +274,11 @@ describe AutocompleteController do context 'authorized projects apply limit' do before do - authorized_project2 = create(:project) - authorized_project3 = create(:project) - - authorized_project.add_maintainer(user) - authorized_project2.add_maintainer(user) - authorized_project3.add_maintainer(user) + allow(Kaminari.config).to receive(:default_per_page).and_return(2) - stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 + create_list(:project, 2) do |project| + project.add_maintainer(user) + end end describe 'GET #projects with project ID' do @@ -291,7 +288,7 @@ describe AutocompleteController do it 'returns projects' do expect(json_response).to be_kind_of(Array) - expect(json_response.size).to eq 2 # Of a total of 3 + expect(json_response.size).to eq(Kaminari.config.default_per_page) end end end diff --git a/spec/finders/autocomplete/group_finder_spec.rb b/spec/finders/autocomplete/group_finder_spec.rb new file mode 100644 index 00000000000..d7cb2c3bbe2 --- /dev/null +++ b/spec/finders/autocomplete/group_finder_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Autocomplete::GroupFinder do + let(:user) { create(:user) } + + describe '#execute' do + context 'with a project' do + it 'returns nil' do + project = create(:project) + + expect(described_class.new(user, project).execute).to be_nil + end + end + + context 'without a group ID' do + it 'returns nil' do + expect(described_class.new(user).execute).to be_nil + end + end + + context 'with an empty String as the group ID' do + it 'returns nil' do + expect(described_class.new(user, nil, group_id: '').execute).to be_nil + end + end + + context 'without a project and with a group ID' do + it 'raises ActiveRecord::RecordNotFound if the group does not exist' do + finder = described_class.new(user, nil, group_id: 1) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ActiveRecord::RecordNotFound if the user can not read the group' do + group = create(:group, :private) + finder = described_class.new(user, nil, group_id: group.id) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ActiveRecord::RecordNotFound if an anonymous user can not read the group' do + group = create(:group, :private) + finder = described_class.new(nil, nil, group_id: group.id) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'returns the group if it exists and is readable' do + group = create(:group) + finder = described_class.new(user, nil, group_id: group.id) + + expect(finder.execute).to eq(group) + end + end + end +end diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/autocomplete/move_to_project_finder_spec.rb index 1b3f44cced1..c3bc410a7f6 100644 --- a/spec/finders/move_to_project_finder_spec.rb +++ b/spec/finders/autocomplete/move_to_project_finder_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MoveToProjectFinder do +describe Autocomplete::MoveToProjectFinder do let(:user) { create(:user) } let(:project) { create(:project) } @@ -10,14 +10,14 @@ describe MoveToProjectFinder do let(:developer_project) { create(:project) } let(:maintainer_project) { create(:project) } - subject { described_class.new(user) } - describe '#execute' do context 'filter' do it 'does not return projects under Gitlab::Access::REPORTER' do guest_project.add_guest(user) - expect(subject.execute(project)).to be_empty + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute).to be_empty end it 'returns projects equal or above Gitlab::Access::REPORTER ordered by id in descending order' do @@ -25,13 +25,17 @@ describe MoveToProjectFinder do developer_project.add_developer(user) maintainer_project.add_maintainer(user) - expect(subject.execute(project).to_a).to eq([maintainer_project, developer_project, reporter_project]) + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute.to_a).to eq([maintainer_project, developer_project, reporter_project]) end it 'does not include the source project' do project.add_reporter(user) - expect(subject.execute(project).to_a).to be_empty + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute.to_a).to be_empty end it 'does not return archived projects' do @@ -40,7 +44,9 @@ describe MoveToProjectFinder do other_reporter_project = create(:project) other_reporter_project.add_reporter(user) - expect(subject.execute(project).to_a).to eq([other_reporter_project]) + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute.to_a).to eq([other_reporter_project]) end it 'does not return projects for which issues are disabled' do @@ -49,39 +55,42 @@ describe MoveToProjectFinder do other_reporter_project = create(:project) other_reporter_project.add_reporter(user) - expect(subject.execute(project).to_a).to eq([other_reporter_project]) + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute.to_a).to eq([other_reporter_project]) end it 'returns a page of projects ordered by id in descending order' do - stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 + allow(Kaminari.config).to receive(:default_per_page).and_return(2) - reporter_project.add_reporter(user) - developer_project.add_developer(user) - maintainer_project.add_maintainer(user) + projects = create_list(:project, 2) do |project| + project.add_developer(user) + end - expect(subject.execute(project).to_a).to eq([maintainer_project, developer_project]) + finder = described_class.new(user, project_id: project.id) + page = finder.execute.to_a + + expect(page.length).to eq(Kaminari.config.default_per_page) + expect(page[0]).to eq(projects.last) end it 'returns projects after the given offset id' do - stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 - reporter_project.add_reporter(user) developer_project.add_developer(user) maintainer_project.add_maintainer(user) - expect(subject.execute(project, search: nil, offset_id: maintainer_project.id).to_a).to eq([developer_project, reporter_project]) - expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project]) - expect(subject.execute(project, search: nil, offset_id: reporter_project.id).to_a).to be_empty - end - end + expect(described_class.new(user, project_id: project.id, offset_id: maintainer_project.id).execute.to_a) + .to eq([developer_project, reporter_project]) - context 'search' do - it 'uses Project#search' do - expect(user).to receive_message_chain(:projects_where_can_admin_issues, :search) { Project.all } + expect(described_class.new(user, project_id: project.id, offset_id: developer_project.id).execute.to_a) + .to eq([reporter_project]) - subject.execute(project, search: 'wadus') + expect(described_class.new(user, project_id: project.id, offset_id: reporter_project.id).execute.to_a) + .to be_empty end + end + context 'search' do it 'returns projects matching a search query' do foo_project = create(:project) foo_project.add_maintainer(user) @@ -89,8 +98,11 @@ describe MoveToProjectFinder do wadus_project = create(:project, name: 'wadus') wadus_project.add_maintainer(user) - expect(subject.execute(project).to_a).to eq([wadus_project, foo_project]) - expect(subject.execute(project, search: 'wadus').to_a).to eq([wadus_project]) + expect(described_class.new(user, project_id: project.id).execute.to_a) + .to eq([wadus_project, foo_project]) + + expect(described_class.new(user, project_id: project.id, search: 'wadus').execute.to_a) + .to eq([wadus_project]) end end end diff --git a/spec/finders/autocomplete/project_finder_spec.rb b/spec/finders/autocomplete/project_finder_spec.rb new file mode 100644 index 00000000000..207d0598c28 --- /dev/null +++ b/spec/finders/autocomplete/project_finder_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Autocomplete::ProjectFinder do + let(:user) { create(:user) } + + describe '#execute' do + context 'without a project ID' do + it 'returns nil' do + expect(described_class.new(user).execute).to be_nil + end + end + + context 'with an empty String as the project ID' do + it 'returns nil' do + expect(described_class.new(user, project_id: '').execute).to be_nil + end + end + + context 'with a project ID' do + it 'raises ActiveRecord::RecordNotFound if the project does not exist' do + finder = described_class.new(user, project_id: 1) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ActiveRecord::RecordNotFound if the user can not read the project' do + project = create(:project, :private) + + finder = described_class.new(user, project_id: project.id) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ActiveRecord::RecordNotFound if an anonymous user can not read the project' do + project = create(:project, :private) + + finder = described_class.new(nil, project_id: project.id) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'returns the project if it exists and is readable' do + project = create(:project, :private) + + project.add_maintainer(user) + + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute).to eq(project) + end + end + end +end diff --git a/spec/finders/autocomplete_users_finder_spec.rb b/spec/finders/autocomplete/users_finder_spec.rb index dcf9111776e..abd0d6b5185 100644 --- a/spec/finders/autocomplete_users_finder_spec.rb +++ b/spec/finders/autocomplete/users_finder_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe AutocompleteUsersFinder do +describe Autocomplete::UsersFinder do describe '#execute' do let!(:user1) { create(:user, username: 'johndoe') } let!(:user2) { create(:user, :blocked, username: 'notsorandom') } diff --git a/spec/finders/awarded_emoji_finder_spec.rb b/spec/finders/awarded_emoji_finder_spec.rb new file mode 100644 index 00000000000..d4479df7418 --- /dev/null +++ b/spec/finders/awarded_emoji_finder_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AwardedEmojiFinder do + describe '#execute' do + it 'returns an Array containing the awarded emoji names' do + user = create(:user) + + create(:award_emoji, user: user, name: 'thumbsup') + create(:award_emoji, user: user, name: 'thumbsup') + create(:award_emoji, user: user, name: 'thumbsdown') + + awarded = described_class.new(user).execute + + expect(awarded).to eq([{ name: 'thumbsup' }, { name: 'thumbsdown' }]) + end + + it 'returns an empty Array when no user is given' do + awarded = described_class.new.execute + + expect(awarded).to be_empty + end + end +end diff --git a/spec/finders/user_finder_spec.rb b/spec/finders/user_finder_spec.rb new file mode 100644 index 00000000000..e53aa50dd33 --- /dev/null +++ b/spec/finders/user_finder_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe UserFinder do + describe '#execute' do + context 'when the user exists' do + it 'returns the user' do + user = create(:user) + found = described_class.new(id: user.id).execute + + expect(found).to eq(user) + end + end + + context 'when the user does not exist' do + it 'returns nil' do + found = described_class.new(id: 1).execute + + expect(found).to be_nil + end + end + end + + describe '#execute!' do + context 'when the user exists' do + it 'returns the user' do + user = create(:user) + found = described_class.new(id: user.id).execute! + + expect(found).to eq(user) + end + end + + context 'when the user does not exist' do + it 'raises ActiveRecord::RecordNotFound' do + finder = described_class.new(id: 1) + + expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb index b909e04dfc3..3f52091698c 100644 --- a/spec/models/award_emoji_spec.rb +++ b/spec/models/award_emoji_spec.rb @@ -77,4 +77,27 @@ describe AwardEmoji do end end end + + describe '.award_counts_for_user' do + let(:user) { create(:user) } + + before do + create(:award_emoji, user: user, name: 'thumbsup') + create(:award_emoji, user: user, name: 'thumbsup') + create(:award_emoji, user: user, name: 'thumbsdown') + create(:award_emoji, user: user, name: '+1') + end + + it 'returns the awarded emoji in descending order' do + awards = described_class.award_counts_for_user(user) + + expect(awards).to eq('thumbsup' => 2, 'thumbsdown' => 1, '+1' => 1) + end + + it 'limits the returned number of rows' do + awards = described_class.award_counts_for_user(user, 1) + + expect(awards).to eq('thumbsup' => 2) + end + end end diff --git a/spec/models/concerns/optionally_search_spec.rb b/spec/models/concerns/optionally_search_spec.rb new file mode 100644 index 00000000000..ff4212ddf18 --- /dev/null +++ b/spec/models/concerns/optionally_search_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe OptionallySearch do + let(:model) do + Class.new(ActiveRecord::Base) do + self.table_name = 'users' + + include OptionallySearch + end + end + + describe '.search' do + it 'raises NotImplementedError' do + expect { model.search('foo') }.to raise_error(NotImplementedError) + end + end + + describe '.optionally_search' do + context 'when a query is given' do + it 'delegates to the search method' do + expect(model) + .to receive(:search) + .with('foo') + + model.optionally_search('foo') + end + end + + context 'when no query is given' do + it 'returns the current relation' do + expect(model.optionally_search).to be_a_kind_of(ActiveRecord::Relation) + end + end + + context 'when an empty query is given' do + it 'returns the current relation' do + expect(model.optionally_search('')) + .to be_a_kind_of(ActiveRecord::Relation) + end + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index d8a5e5f6869..56c07f5793b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1478,6 +1478,53 @@ describe Project do end end + describe '.optionally_search' do + let(:project) { create(:project) } + + it 'searches for projects matching the query if one is given' do + relation = described_class.optionally_search(project.name) + + expect(relation).to eq([project]) + end + + it 'returns the current relation if no search query is given' do + relation = described_class.where(id: project.id) + + expect(relation.optionally_search).to eq(relation) + end + end + + describe '.paginate_in_descending_order_using_id' do + let!(:project1) { create(:project) } + let!(:project2) { create(:project) } + + it 'orders the relation in descending order' do + expect(described_class.paginate_in_descending_order_using_id) + .to eq([project2, project1]) + end + + it 'applies a limit to the relation' do + expect(described_class.paginate_in_descending_order_using_id(limit: 1)) + .to eq([project2]) + end + + it 'limits projects by and ID when given' do + expect(described_class.paginate_in_descending_order_using_id(before: project2.id)) + .to eq([project1]) + end + end + + describe '.including_namespace_and_owner' do + it 'eager loads the namespace and namespace owner' do + create(:project) + + row = described_class.eager_load_namespace_and_owner.to_a.first + recorder = ActiveRecord::QueryRecorder.new { row.namespace.owner } + + expect(recorder.count).to be_zero + end + end + describe '#expire_caches_before_rename' do let(:project) { create(:project, :repository) } let(:repo) { double(:repo, exists?: true) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f5e2c977104..9763477a711 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -346,17 +346,55 @@ describe User do end end - describe '.todo_authors' do - it 'filters users' do - create :user - user_2 = create :user - user_3 = create :user - current_user = create :user - create(:todo, user: current_user, author: user_2, state: :done) - create(:todo, user: current_user, author: user_3, state: :pending) + describe '.limit_to_todo_authors' do + context 'when filtering by todo authors' do + let(:user1) { create(:user) } + let(:user2) { create(:user) } - expect(described_class.todo_authors(current_user.id, 'pending')).to eq [user_3] - expect(described_class.todo_authors(current_user.id, 'done')).to eq [user_2] + before do + create(:todo, user: user1, author: user1, state: :done) + create(:todo, user: user2, author: user2, state: :pending) + end + + it 'only returns users that have authored todos' do + users = described_class.limit_to_todo_authors( + user: user2, + with_todos: true, + todo_state: :pending + ) + + expect(users).to eq([user2]) + end + + it 'ignores users that do not have a todo in the matching state' do + users = described_class.limit_to_todo_authors( + user: user1, + with_todos: true, + todo_state: :pending + ) + + expect(users).to be_empty + end + end + + context 'when not filtering by todo authors' do + it 'returns the input relation' do + user1 = create(:user) + user2 = create(:user) + rel = described_class.limit_to_todo_authors(user: user1) + + expect(rel).to include(user1, user2) + end + end + + context 'when no user is provided' do + it 'returns the input relation' do + user1 = create(:user) + user2 = create(:user) + rel = described_class.limit_to_todo_authors + + expect(rel).to include(user1, user2) + end end end end @@ -2901,4 +2939,86 @@ describe User do let(:uploader_class) { AttachmentUploader } end end + + describe '.union_with_user' do + context 'when no user ID is provided' do + it 'returns the input relation' do + user = create(:user) + + expect(described_class.union_with_user).to eq([user]) + end + end + + context 'when a user ID is provided' do + it 'includes the user object in the returned relation' do + user1 = create(:user) + user2 = create(:user) + users = described_class.where(id: user1.id).union_with_user(user2.id) + + expect(users).to include(user1) + expect(users).to include(user2) + end + + it 'does not re-apply any WHERE conditions on the outer query' do + relation = described_class.where(id: 1).union_with_user(2) + + expect(relation.arel.where_sql).to be_nil + end + end + end + + describe '.optionally_search' do + context 'using nil as the argument' do + it 'returns the current relation' do + user = create(:user) + + expect(described_class.optionally_search).to eq([user]) + end + end + + context 'using an empty String as the argument' do + it 'returns the current relation' do + user = create(:user) + + expect(described_class.optionally_search('')).to eq([user]) + end + end + + context 'using a non-empty String' do + it 'returns users matching the search query' do + user1 = create(:user) + create(:user) + + expect(described_class.optionally_search(user1.name)).to eq([user1]) + end + end + end + + describe '.where_not_in' do + context 'without an argument' do + it 'returns the current relation' do + user = create(:user) + + expect(described_class.where_not_in).to eq([user]) + end + end + + context 'using a list of user IDs' do + it 'excludes the users from the returned relation' do + user1 = create(:user) + user2 = create(:user) + + expect(described_class.where_not_in([user2.id])).to eq([user1]) + end + end + end + + describe '.reorder_by_name' do + it 'reorders the input relation' do + user1 = create(:user, name: 'A') + user2 = create(:user, name: 'B') + + expect(described_class.reorder_by_name).to eq([user1, user2]) + end + end end diff --git a/spec/serializers/move_to_project_entity_spec.rb b/spec/serializers/move_to_project_entity_spec.rb new file mode 100644 index 00000000000..ac495eadb68 --- /dev/null +++ b/spec/serializers/move_to_project_entity_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MoveToProjectEntity do + describe '#as_json' do + let(:project) { build(:project, id: 1) } + + subject { described_class.new(project).as_json } + + it 'includes the project ID' do + expect(subject[:id]).to eq(project.id) + end + + it 'includes the full path' do + expect(subject[:name_with_namespace]).to eq(project.name_with_namespace) + end + end +end diff --git a/spec/serializers/move_to_project_serializer_spec.rb b/spec/serializers/move_to_project_serializer_spec.rb new file mode 100644 index 00000000000..841ac969eeb --- /dev/null +++ b/spec/serializers/move_to_project_serializer_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MoveToProjectSerializer do + describe '#represent' do + it 'includes the name and name with namespace' do + project = build(:project, id: 1) + output = described_class.new.represent(project) + + expect(output).to include(:id, :name_with_namespace) + end + end +end |