diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 9d21e285e74c7917f83e36796bd37575568f3f86..534a45ed68010aa951652cd3f4a9861479af1d30 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -1,71 +1,41 @@
 class AutocompleteController < ApplicationController
-  AWARD_EMOJI_MAX = 100
+  prepend EE::AutocompleteController
 
   skip_before_action :authenticate_user!, only: [:users, :award_emojis]
-  before_action :load_project, only: [:users, :project_groups]
-  before_action :load_group, only: [:users]
 
   def users
-    @users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute
+    project = Autocomplete::ProjectFinder
+      .new(current_user, params)
+      .execute
 
-    render json: UserSerializer.new.represent(@users)
-  end
+    group = Autocomplete::GroupFinder
+      .new(current_user, project, params)
+      .execute
+
+    users = Autocomplete::UsersFinder
+      .new(params: params, current_user: current_user, project: project, group: group)
+      .execute
 
-  def project_groups
-    render json: @project.invited_groups, only: [:id, :name], methods: [:avatar_url]
+    render json: UserSerializer.new.represent(users)
   end
 
   def user
-    @user = User.find(params[:id])
-    render json: UserSerializer.new.represent(@user)
+    user = UserFinder.new(params).execute!
+
+    render json: UserSerializer.new.represent(user)
   end
 
+  # Displays projects to use for the dropdown when moving a resource from one
+  # project to another.
   def projects
-    project = Project.find_by_id(params[:project_id])
-    projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id])
+    projects = Autocomplete::MoveToProjectFinder
+      .new(current_user, params)
+      .execute
 
-    render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
+    render json: MoveToProjectSerializer.new.represent(projects)
   end
 
   def award_emojis
-    emoji_with_count = AwardEmoji
-      .limit(AWARD_EMOJI_MAX)
-      .where(user: current_user)
-      .group(:name)
-      .order('count_all DESC, name ASC')
-      .count
-
-    # 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 } }
-  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)
-
-        group
-      end
-    end
-  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)
-
-        project
-      end
-    end
-  end
-
-  def projects_finder
-    MoveToProjectFinder.new(current_user)
+    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 0000000000000000000000000000000000000000..dd97ac4c81784addb8b9054535e47c14c935bf37
--- /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 0000000000000000000000000000000000000000..edaf74c5f9293863f788464de5994a58cad97d92
--- /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 0000000000000000000000000000000000000000..3a4696f4c2ef93941d8e7ed32d8b83d704baf163
--- /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 0000000000000000000000000000000000000000..dceed82763375acad4b0a0b641b8579336788568
--- /dev/null
+++ b/app/finders/autocomplete/users_finder.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Autocomplete
+  class UsersFinder
+    prepend EE::Autocomplete::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 8a7ba6d2e0c0aa70713ca83c17853271bd379ce3..0000000000000000000000000000000000000000
--- a/app/finders/autocomplete_users_finder.rb
+++ /dev/null
@@ -1,101 +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
-
-  # EE
-  attr_reader :skip_ldap
-
-  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
-
-    # EE
-    @skip_ldap = params[:skip_ldap]
-  end
-
-  def execute
-    items = find_users
-
-    # EE
-    items = items.non_ldap if skip_ldap == 'true'
-
-    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)
-
-    # EE
-    items = load_users_by_push_ability(items)
-
-    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
-
-  # EE
-  def load_users_by_push_ability(items)
-    return items unless project
-
-    ability = push_ability
-    return items if ability.blank?
-
-    items.to_a
-      .select { |user| user.can?(ability, project) }
-  end
-
-  def push_ability
-    if params[:push_code_to_protected_branches].present?
-      :push_code_to_protected_branches
-    elsif params[:push_code].present?
-      :push_code
-    end
-  end
-  # EE
-end
diff --git a/app/finders/awarded_emoji_finder.rb b/app/finders/awarded_emoji_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f0cc17f3b262e010ded0061a9b18a0a519f17b6e
--- /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 038d5565a1ec5ab2b6a25d49156bf9e2f1070007..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..484a93c987347e4564e1a42991790b1809690bf0
--- /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 99c7866d63661318b60fc19151d27e6568d8b7db..ddc516ccb60489a27ae22dfd4d7b1a019dc08128 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -28,6 +28,23 @@ def votes_for_collection(ids, type)
         .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 0000000000000000000000000000000000000000..dec97b7dee83373817535609deca5ede063d87e8
--- /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 f8c336ae835879a8fb98fed98927a69c4c91dab8..fd855f1c1cf9eb6c814c47186ff3577eae27094c 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
 
   # EE specific modules
@@ -391,6 +392,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 d2823016308a7922c5af9bdba04f8dad63597412..3a80b60be7ba3944fbe56ca45329067dd5615191 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
 
   prepend EE::User
 
@@ -262,11 +263,41 @@ def inactive_message
   scope :with_provider, ->(provider) do
     joins(:identities).where(identities: { provider: provider })
   end
-  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)
@@ -378,6 +409,18 @@ def search(query)
       ).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 0000000000000000000000000000000000000000..dac1124b0b38ab7a90412456c7ff99c4cc84f70d
--- /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 0000000000000000000000000000000000000000..6a59317505c6c65c8ac88a8043d11aa6507c6bae
--- /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/ee/app/controllers/ee/autocomplete_controller.rb b/ee/app/controllers/ee/autocomplete_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..30648181a8379343ad47a9ddc09f51b6cf32c2d5
--- /dev/null
+++ b/ee/app/controllers/ee/autocomplete_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module EE
+  module AutocompleteController
+    def project_groups
+      groups = ::Autocomplete::ProjectInvitedGroupsFinder
+        .new(current_user, params)
+        .execute
+
+      render json: InvitedGroupSerializer.new.represent(groups)
+    end
+  end
+end
diff --git a/ee/app/finders/autocomplete/project_invited_groups_finder.rb b/ee/app/finders/autocomplete/project_invited_groups_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f8c05b2c6706ab779ae20db294fb6f55974d925b
--- /dev/null
+++ b/ee/app/finders/autocomplete/project_invited_groups_finder.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Autocomplete
+  class ProjectInvitedGroupsFinder
+    attr_reader :current_user, :params
+
+    # 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 supported parameters are those supported by
+    # `Autocomplete::ProjectFinder`.
+    def initialize(current_user, params = {})
+      @current_user = current_user
+      @params = params
+    end
+
+    def execute
+      project = ::Autocomplete::ProjectFinder
+        .new(current_user, params)
+        .execute
+
+      project ? project.invited_groups : Group.none
+    end
+  end
+end
diff --git a/ee/app/finders/ee/autocomplete/users_finder.rb b/ee/app/finders/ee/autocomplete/users_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b277dfff97bd0464cc9ab292d37aa251975daede
--- /dev/null
+++ b/ee/app/finders/ee/autocomplete/users_finder.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module EE
+  module Autocomplete
+    module UsersFinder
+      extend ::Gitlab::Utils::Override
+
+      attr_reader :skip_ldap, :push_code_to_protected_branches, :push_code
+
+      override :initialize
+      def initialize(params:, current_user:, project:, group:)
+        super
+
+        @skip_ldap = params[:skip_ldap]
+        @push_code_to_protected_branches = params[:push_code_to_protected_branches]
+        @push_code = params[:push_code]
+      end
+
+      override :find_users
+      def find_users
+        users = super
+
+        skip_ldap == 'true' ? users.non_ldap : users
+      end
+
+      override :limited_users
+      def limited_users
+        load_users_by_push_ability(super)
+      end
+
+      def load_users_by_push_ability(items)
+        return items unless project
+
+        ability = push_ability
+        return items if ability.blank?
+
+        items.to_a
+          .select { |user| user.can?(ability, project) }
+      end
+
+      def push_ability
+        if push_code_to_protected_branches.present?
+          :push_code_to_protected_branches
+        elsif push_code.present?
+          :push_code
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/serializers/invited_group_entity.rb b/ee/app/serializers/invited_group_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..263b5820a20ba4cf587b00466e2d13f991d4064d
--- /dev/null
+++ b/ee/app/serializers/invited_group_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class InvitedGroupEntity < Grape::Entity
+  expose :id
+  expose :name
+  expose :avatar_url
+end
diff --git a/ee/app/serializers/invited_group_serializer.rb b/ee/app/serializers/invited_group_serializer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..00734939985b697cf42e6d6af01fdebede06e80c
--- /dev/null
+++ b/ee/app/serializers/invited_group_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class InvitedGroupSerializer < BaseSerializer
+  entity InvitedGroupEntity
+end
diff --git a/ee/spec/finders/autocomplete/project_invited_groups_finder_spec.rb b/ee/spec/finders/autocomplete/project_invited_groups_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1b46de28c38b8bb8179e9e568d0ad7e07979a056
--- /dev/null
+++ b/ee/spec/finders/autocomplete/project_invited_groups_finder_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Autocomplete::ProjectInvitedGroupsFinder do
+  let(:user) { create(:user) }
+
+  describe '#execute' do
+    context 'without a project ID' do
+      it 'returns an empty relation' do
+        expect(described_class.new(user).execute).to be_empty
+      end
+    end
+
+    context 'with a project ID' do
+      it 'returns the groups invited to the project' do
+        project = create(:project, :public)
+        group = create(:group)
+
+        create(:project_group_link, project: project, group: group)
+
+        expect(described_class.new(user, project_id: project.id).execute)
+          .to eq([group])
+      end
+    end
+  end
+end
diff --git a/ee/spec/serializers/invited_group_entity_spec.rb b/ee/spec/serializers/invited_group_entity_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..82750e2a9a2648ac2f9918d7fdaf3be08c51aa96
--- /dev/null
+++ b/ee/spec/serializers/invited_group_entity_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe InvitedGroupEntity do
+  describe '#as_json' do
+    let(:group) { build(:group, id: 1) }
+
+    subject { described_class.new(group).as_json }
+
+    it 'includes the group ID' do
+      expect(subject[:id]).to eq(group.id)
+    end
+
+    it 'includes the group name' do
+      expect(subject[:name]).to eq(group.name)
+    end
+
+    it 'includes the group avatar URL' do
+      expect(subject[:avatar_url]).to eq(group.avatar_url)
+    end
+  end
+end
diff --git a/ee/spec/serializers/invited_group_serializer_spec.rb b/ee/spec/serializers/invited_group_serializer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c36f24ce5444adb45f58ccf8346e506f71918ee1
--- /dev/null
+++ b/ee/spec/serializers/invited_group_serializer_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe InvitedGroupSerializer do
+  describe '#represent' do
+    it 'includes the id, name, and avatar URL' do
+      group = build(:group, id: 1)
+      output = described_class.new.represent(group)
+
+      expect(output).to include(:id, :name, :avatar_url)
+    end
+  end
+end
diff --git a/lib/gitlab/github_import/importer/diff_note_importer.rb b/lib/gitlab/github_import/importer/diff_note_importer.rb
index 8274f37d3580ff8a10d9405e4b53bca38174ee75..d562958e95551b18fcb83c71e17e6e90a0dab1c1 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 @@ def initialize(note, project, client)
           @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 ead4215810f03c7fb2831961274f2652a4344555..cb4d7a6a0b6ec3f21c8909c71ed8b37f357c5a35 100644
--- a/lib/gitlab/github_import/importer/issue_importer.rb
+++ b/lib/gitlab/github_import/importer/issue_importer.rb
@@ -19,7 +19,7 @@ def initialize(issue, project, client)
           @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 c890f2df360559f15de0da699ba9209a450d7e8f..2b06d1b3baf170788ff06f61689d1c846d866f64 100644
--- a/lib/gitlab/github_import/importer/note_importer.rb
+++ b/lib/gitlab/github_import/importer/note_importer.rb
@@ -13,7 +13,7 @@ def initialize(note, project, client)
           @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 e4b49d2143aee12b98dff0da16b4cb735cbca295..ed17aa54373b848f1b8d4ca7663afdd79752cd98 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 @@ def initialize(pull_request, project, client)
           @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 00adc9663aa8317fa6171118df1ed9f0d5d59efc..1402ddef150a404a1987ea2c56342424af2a60df 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -274,14 +274,11 @@
 
     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 @@
 
         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 0000000000000000000000000000000000000000..d7cb2c3bbe2d9fe6dd884a68b24ebdd09ac28b20
--- /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
similarity index 52%
rename from spec/finders/move_to_project_finder_spec.rb
rename to spec/finders/autocomplete/move_to_project_finder_spec.rb
index 1b3f44cced1fe10ac6f8b7834e1d4c79830c49a3..c3bc410a7f673e82b98507706ec13d006fe4d647 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 @@
   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 @@
         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 @@
         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 @@
         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 @@
         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 0000000000000000000000000000000000000000..207d0598c28b0c0bf4edead7056a0de2244cbe4f
--- /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
similarity index 99%
rename from spec/finders/autocomplete_users_finder_spec.rb
rename to spec/finders/autocomplete/users_finder_spec.rb
index dcf9111776eabfb76109f9588a8fe17f55a7410f..abd0d6b51853f9671702c3208ccd20abd8574d2c 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 0000000000000000000000000000000000000000..d4479df7418cc845767befa052b70861602d7611
--- /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 0000000000000000000000000000000000000000..e53aa50dd33d8871d36b21da385b5d1ba2d8ed34
--- /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 b909e04dfc3eab7e335b19af619fb6e5cca4a935..3f52091698c2c4da4489cc826352e29a70d9790c 100644
--- a/spec/models/award_emoji_spec.rb
+++ b/spec/models/award_emoji_spec.rb
@@ -77,4 +77,27 @@
       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 0000000000000000000000000000000000000000..ff4212ddf18c5263305c27e114dc198bc521141c
--- /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 8a91c0f10eb2bcd9b95fd78cd69719aa6294f405..241d3f549802c937ad68f1cf28760268960214ca 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1607,6 +1607,53 @@ def create_pipeline
     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 fd7b3385a2071e8cf681093346cc5bcf76646270..1569df4b2c8def4193ba2cfd99e2acffc2f3d145 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -374,17 +374,55 @@
       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
@@ -3004,4 +3042,86 @@ def access_levels(groups)
       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 0000000000000000000000000000000000000000..ac495eadb6802a50576815e5f6de640f324f8d88
--- /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 0000000000000000000000000000000000000000..841ac969eeb84ad31bebaa032f09b7f8088577fb
--- /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