Commit 6f3c4901 authored by Yorick Peterse's avatar Yorick Peterse

Refactor AutocompleteController

This refactors the AutocompleteController according to the guidelines
and boundaries discussed in
https://gitlab.com/gitlab-org/gitlab-ce/issues/49653. Specifically,
ActiveRecord logic is moved to different finders, which are then used in
the controller. View logic in turn is moved to presenters, instead of
directly using ActiveRecord's "to_json" method.

The finder MoveToProjectFinder is also adjusted according to the
abstraction guidelines and boundaries, resulting in a much more simple
finder.

By using finders (and other abstractions) more actively, we can push a
lot of logic out of the controller. We also remove the need for various
"before_action" hooks, though this could be achieved without using
finders as well.

The various finders related to AutcompleteController have also been
moved into a namespace. This removes the need for calling everything
"AutocompleteSmurfFinder", instead you can use
"Autocomplete::SmurfFinder".
parent 0a73c1c5
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
# 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
# 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
# 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
# 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
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
# 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
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
# 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
......@@ -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?
......
# 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
......@@ -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)
......
......@@ -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.
......
# frozen_string_literal: true
class MoveToProjectEntity < Grape::Entity
expose :id
expose :name_with_namespace
end
# frozen_string_literal: true
class MoveToProjectSerializer < BaseSerializer
entity MoveToProjectEntity
end
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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)
......
......@@ -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
......
# 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
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
......
# 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
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') }
......
# 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
# 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
......@@ -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
# 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
......@@ -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) }
......
......@@ -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
# 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
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment