Commit dd860654 authored by Alex Kalderimis's avatar Alex Kalderimis Committed by Bob Van Landuyt

Move resolver logic to finder

This moves the core logic of the merge request resolver and the issues
resolver into a finder (technically a composition of finders), so that
we can re-use it in other resolvers/mutations without needing to
instantiate an entire resolver and run the full resolver life-cycle each
time.
parent 4244baa2
...@@ -110,7 +110,9 @@ class IssuableFinder ...@@ -110,7 +110,9 @@ class IssuableFinder
def group def group
strong_memoize(:group) do strong_memoize(:group) do
if params[:group_id].present? if params[:group_id].is_a?(Group)
params[:group_id]
elsif params[:group_id].present?
Group.find(params[:group_id]) Group.find(params[:group_id])
else else
nil nil
......
...@@ -27,19 +27,14 @@ class IssuesFinder ...@@ -27,19 +27,14 @@ class IssuesFinder
end end
def user_can_see_all_confidential_issues? def user_can_see_all_confidential_issues?
return @user_can_see_all_confidential_issues if defined?(@user_can_see_all_confidential_issues) strong_memoize(:user_can_see_all_confidential_issues) do
parent = project? ? project : group
return @user_can_see_all_confidential_issues = false if current_user.blank? if parent
return @user_can_see_all_confidential_issues = true if current_user.can_read_all_resources? Ability.allowed?(current_user, :read_confidential_issues, parent)
@user_can_see_all_confidential_issues =
if project? && project
project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL
elsif group
group.max_member_access_for_user(current_user) >= CONFIDENTIAL_ACCESS_LEVEL
else else
false Ability.allowed?(current_user, :read_all_resources)
end end
end
end end
def user_cannot_see_confidential_issues? def user_cannot_see_confidential_issues?
......
...@@ -9,30 +9,31 @@ module Mutations ...@@ -9,30 +9,31 @@ module Mutations
end end
def resolve_issuable(type:, parent_path:, iid:) def resolve_issuable(type:, parent_path:, iid:)
parent = resolve_issuable_parent(type, parent_path) parent = ::Gitlab::Graphql::Lazy.force(resolve_issuable_parent(type, parent_path))
key = type == :merge_request ? :iids : :iid return unless parent.present?
args = { key => iid.to_s }
resolver = issuable_resolver(type, parent, context) finder = issuable_finder(type, iids: [iid])
ready, early_return = resolver.ready?(**args) Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).find_all.first
return early_return unless ready
resolver.resolve(**args)
end end
private private
def issuable_resolver(type, parent, context) def issuable_finder(type, args)
resolver_class = "Resolvers::#{type.to_s.classify.pluralize}Resolver".constantize case type
when :merge_request
resolver_class.single.new(object: parent, context: context, field: nil) MergeRequestsFinder.new(current_user, args)
when :issue
IssuesFinder.new(current_user, args)
else
raise "Unsupported type: #{type}"
end
end end
def resolve_issuable_parent(type, parent_path) def resolve_issuable_parent(type, parent_path)
return unless parent_path.present?
return unless type == :issue || type == :merge_request return unless type == :issue || type == :merge_request
resolve_project(full_path: parent_path) if parent_path.present? resolve_project(full_path: parent_path)
end end
end end
end end
......
...@@ -83,5 +83,10 @@ module Resolvers ...@@ -83,5 +83,10 @@ module Resolvers
def current_user def current_user
context[:current_user] context[:current_user]
end end
# Overridden in sub-classes (see .single, .last)
def select_result(results)
results
end
end end
end end
...@@ -11,16 +11,10 @@ module ResolvesMergeRequests ...@@ -11,16 +11,10 @@ module ResolvesMergeRequests
end end
def resolve_with_lookahead(**args) def resolve_with_lookahead(**args)
args[:iids] = Array.wrap(args[:iids]) if args[:iids] mr_finder = MergeRequestsFinder.new(current_user, args.compact)
args.compact! finder = Gitlab::Graphql::Loaders::IssuableLoader.new(project, mr_finder)
if project && args.keys == [:iids] select_result(finder.batching_find_all { |query| apply_lookahead(query) })
batch_load_merge_requests(args[:iids])
else
args[:project_id] ||= project
apply_lookahead(MergeRequestsFinder.new(current_user, args).execute)
end.then(&(single? ? :first : :itself))
end end
def ready?(**args) def ready?(**args)
...@@ -35,22 +29,6 @@ module ResolvesMergeRequests ...@@ -35,22 +29,6 @@ module ResolvesMergeRequests
private private
def batch_load_merge_requests(iids)
iids.map { |iid| batch_load(iid) }.select(&:itself) # .compact doesn't work on BatchLoader
end
# rubocop: disable CodeReuse/ActiveRecord
def batch_load(iid)
BatchLoader::GraphQL.for(iid.to_s).batch(key: project) do |iids, loader, args|
query = args[:key].merge_requests.where(iid: iids)
apply_lookahead(query).each do |mr|
loader.call(mr.iid.to_s, mr)
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
def unconditional_includes def unconditional_includes
[:target_project] [:target_project]
end end
......
...@@ -63,18 +63,13 @@ module Resolvers ...@@ -63,18 +63,13 @@ module Resolvers
parent = object.respond_to?(:sync) ? object.sync : object parent = object.respond_to?(:sync) ? object.sync : object
return Issue.none if parent.nil? return Issue.none if parent.nil?
if parent.is_a?(Group)
args[:group_id] = parent.id
else
args[:project_id] = parent.id
end
# Will need to be be made group & namespace aware with # Will need to be be made group & namespace aware with
# https://gitlab.com/gitlab-org/gitlab-foss/issues/54520 # https://gitlab.com/gitlab-org/gitlab-foss/issues/54520
args[:iids] ||= [args[:iid]].compact args[:iids] ||= [args.delete(:iid)].compact if args[:iid]
args[:attempt_project_search_optimizations] = args[:search].present? args[:attempt_project_search_optimizations] = true if args[:search].present?
issues = IssuesFinder.new(context[:current_user], args).execute finder = IssuesFinder.new(current_user, args)
issues = Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all
if non_stable_cursor_sort?(args[:sort]) if non_stable_cursor_sort?(args[:sort])
# Certain complex sorts are not supported by the stable cursor pagination yet. # Certain complex sorts are not supported by the stable cursor pagination yet.
......
...@@ -67,6 +67,10 @@ class GroupPolicy < BasePolicy ...@@ -67,6 +67,10 @@ class GroupPolicy < BasePolicy
enable :update_max_artifacts_size enable :update_max_artifacts_size
end end
rule { can?(:read_all_resources) }.policy do
enable :read_confidential_issues
end
rule { has_projects }.policy do rule { has_projects }.policy do
enable :read_group enable :read_group
end end
......
...@@ -176,6 +176,7 @@ class ProjectPolicy < BasePolicy ...@@ -176,6 +176,7 @@ class ProjectPolicy < BasePolicy
rule { guest | admin }.enable :read_project_for_iids rule { guest | admin }.enable :read_project_for_iids
rule { admin }.enable :update_max_artifacts_size rule { admin }.enable :update_max_artifacts_size
rule { can?(:read_all_resources) }.enable :read_confidential_issues
rule { guest }.enable :guest_access rule { guest }.enable :guest_access
rule { reporter }.enable :reporter_access rule { reporter }.enable :reporter_access
...@@ -257,6 +258,7 @@ class ProjectPolicy < BasePolicy ...@@ -257,6 +258,7 @@ class ProjectPolicy < BasePolicy
enable :read_prometheus enable :read_prometheus
enable :read_metrics_dashboard_annotation enable :read_metrics_dashboard_annotation
enable :metrics_dashboard enable :metrics_dashboard
enable :read_confidential_issues
end end
# We define `:public_user_access` separately because there are cases in gitlab-ee # We define `:public_user_access` separately because there are cases in gitlab-ee
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
class EpicsFinder < IssuableFinder class EpicsFinder < IssuableFinder
include TimeFrameFilter include TimeFrameFilter
include Gitlab::Utils::StrongMemoize
IID_STARTS_WITH_PATTERN = %r{\A(\d)+\z}.freeze IID_STARTS_WITH_PATTERN = %r{\A(\d)+\z}.freeze
...@@ -128,10 +129,15 @@ class EpicsFinder < IssuableFinder ...@@ -128,10 +129,15 @@ class EpicsFinder < IssuableFinder
end end
def group def group
return unless params[:group_id] strong_memoize(:group) do
return @group if defined?(@group) next unless params[:group_id]
@group = Group.find(params[:group_id]) if params[:group_id].is_a?(Group)
params[:group_id]
else
Group.find(params[:group_id])
end
end
end end
def starts_with_iid(items) def starts_with_iid(items)
......
...@@ -8,6 +8,12 @@ module EE ...@@ -8,6 +8,12 @@ module EE
private private
def issuable_finder(type, args)
return EpicsFinder.new(current_user, args) if type == :epic
super
end
override :resolve_issuable_parent override :resolve_issuable_parent
def resolve_issuable_parent(type, parent_path) def resolve_issuable_parent(type, parent_path)
return super unless type == :epic return super unless type == :epic
......
...@@ -12,11 +12,12 @@ RSpec.describe Mutations::ResolvesIssuable do ...@@ -12,11 +12,12 @@ RSpec.describe Mutations::ResolvesIssuable do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:context) { { current_user: user } } let_it_be(:context) { { current_user: user } }
let_it_be(:mutation) { mutation_class.new(object: nil, context: context, field: nil) } let_it_be(:epic) { create(:epic, group: group) }
let(:mutation) { mutation_class.new(object: nil, context: context, field: nil) }
context 'with epics' do context 'with epics' do
let_it_be(:issuable) { create(:epic, group: group) } let(:parent) { issuable.group }
let_it_be(:parent) { issuable.group } let(:issuable) { epic }
before do before do
stub_licensed_features(epics: true) stub_licensed_features(epics: true)
......
...@@ -30,8 +30,7 @@ module Gitlab ...@@ -30,8 +30,7 @@ module Gitlab
end end
def authorized_find!(*args) def authorized_find!(*args)
object = find_object(*args) object = Graphql::Lazy.force(find_object(*args))
object = object.sync if object.respond_to?(:sync)
authorize!(object) authorize!(object)
......
# frozen_string_literal: true
module Gitlab
module Graphql
class Lazy
# Force evaluation of a (possibly) lazy value
def self.force(value)
case value
when ::BatchLoader::GraphQL
value.sync
when ::Concurrent::Promise
value.execute.value
else
value
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Graphql
module Loaders
class IssuableLoader
attr_reader :parent, :issuable_finder
BatchKey = Struct.new(:parent, :finder_class, :current_user)
def initialize(parent, issuable_finder)
@parent = parent
@issuable_finder = issuable_finder
end
def batching_find_all(&with_query)
if issuable_finder.params.keys == ['iids']
batch_load_issuables(issuable_finder.params[:iids], with_query)
else
post_process(find_all, with_query)
end
end
def find_all
issuable_finder.params[parent_param] = parent if parent
issuable_finder.execute
end
private
def parent_param
case parent
when Project
:project_id
when Group
:group_id
else
raise "Unexpected parent: #{parent.class}"
end
end
def post_process(query, with_query)
if with_query
with_query.call(query)
else
query
end
end
def batch_load_issuables(iids, with_query)
Array.wrap(iids).map { |iid| batch_load(iid, with_query) }
end
def batch_load(iid, with_query)
return if parent.nil?
BatchLoader::GraphQL
.for([parent_param, iid.to_s])
.batch(key: batch_key) do |params, loader, args|
batch_key = args[:key]
user = batch_key.current_user
params.group_by(&:first).each do |key, group|
iids = group.map(&:second).uniq
args = { key => batch_key.parent, iids: iids }
query = batch_key.finder_class.new(user, args).execute
post_process(query, with_query).each do |item|
loader.call([key, item.iid.to_s], item)
end
end
end
end
def batch_key
BatchKey.new(parent, issuable_finder.class, issuable_finder.current_user)
end
end
end
end
end
...@@ -3,26 +3,31 @@ ...@@ -3,26 +3,31 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Mutations::ResolvesIssuable do RSpec.describe Mutations::ResolvesIssuable do
include GraphqlHelpers
let_it_be(:mutation_class) do let_it_be(:mutation_class) do
Class.new(Mutations::BaseMutation) do Class.new(Mutations::BaseMutation) do
include Mutations::ResolvesIssuable include Mutations::ResolvesIssuable
end end
end end
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project, :empty_repo) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:context) { { current_user: user } } let_it_be(:context) { { current_user: user } }
let_it_be(:mutation) { mutation_class.new(object: nil, context: context, field: nil) } let(:mutation) { mutation_class.new(object: nil, context: context, field: nil) }
let(:parent) { issuable.project } let(:parent) { issuable.project }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
context 'with issues' do context 'with issues' do
let(:issuable) { create(:issue, project: project) } let(:issuable) { issue }
it_behaves_like 'resolving an issuable in GraphQL', :issue it_behaves_like 'resolving an issuable in GraphQL', :issue
end end
context 'with merge requests' do context 'with merge requests' do
let(:issuable) { create(:merge_request, source_project: project) } let(:issuable) { merge_request }
it_behaves_like 'resolving an issuable in GraphQL', :merge_request it_behaves_like 'resolving an issuable in GraphQL', :merge_request
end end
......
...@@ -101,12 +101,10 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -101,12 +101,10 @@ RSpec.describe Resolvers::IssuesResolver do
end end
it 'uses project search optimization' do it 'uses project search optimization' do
expected_arguments = { expected_arguments = a_hash_including(
search: 'foo', search: 'foo',
attempt_project_search_optimizations: true, attempt_project_search_optimizations: true
iids: [], )
project_id: project.id
}
expect(IssuesFinder).to receive(:new).with(anything, expected_arguments).and_call_original expect(IssuesFinder).to receive(:new).with(anything, expected_arguments).and_call_original
resolve_issues(search: 'foo') resolve_issues(search: 'foo')
...@@ -217,16 +215,32 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -217,16 +215,32 @@ RSpec.describe Resolvers::IssuesResolver do
expect(resolve_issues).to contain_exactly(issue1, issue2) expect(resolve_issues).to contain_exactly(issue1, issue2)
end end
it 'finds a specific issue with iid' do it 'finds a specific issue with iid', :request_store do
expect(resolve_issues(iid: issue1.iid)).to contain_exactly(issue1) result = batch_sync(max_queries: 2) { resolve_issues(iid: issue1.iid) }
expect(result).to contain_exactly(issue1)
end
it 'batches queries that only include IIDs', :request_store do
result = batch_sync(max_queries: 2) do
resolve_issues(iid: issue1.iid) + resolve_issues(iids: issue2.iid)
end
expect(result).to contain_exactly(issue1, issue2)
end end
it 'finds a specific issue with iids' do it 'finds a specific issue with iids', :request_store do
expect(resolve_issues(iids: issue1.iid)).to contain_exactly(issue1) result = batch_sync(max_queries: 2) do
resolve_issues(iids: [issue1.iid])
end
expect(result).to contain_exactly(issue1)
end end
it 'finds multiple issues with iids' do it 'finds multiple issues with iids' do
expect(resolve_issues(iids: [issue1.iid, issue2.iid])) create(:issue, project: project, author: current_user)
expect(batch_sync { resolve_issues(iids: [issue1.iid, issue2.iid]) })
.to contain_exactly(issue1, issue2) .to contain_exactly(issue1, issue2)
end end
...@@ -238,7 +252,7 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -238,7 +252,7 @@ RSpec.describe Resolvers::IssuesResolver do
create(:issue, project: another_project, iid: iid) create(:issue, project: another_project, iid: iid)
end end
expect(resolve_issues(iids: iids)).to contain_exactly(issue1, issue2) expect(batch_sync { resolve_issues(iids: iids) }).to contain_exactly(issue1, issue2)
end end
end end
end end
......
...@@ -22,9 +22,12 @@ RSpec.describe Resolvers::MergeRequestsResolver do ...@@ -22,9 +22,12 @@ RSpec.describe Resolvers::MergeRequestsResolver do
before do before do
project.add_developer(current_user) project.add_developer(current_user)
other_project.add_developer(current_user)
end end
describe '#resolve' do describe '#resolve' do
let(:queries_per_project) { 3 }
context 'no arguments' do context 'no arguments' do
it 'returns all merge requests' do it 'returns all merge requests' do
result = resolve_mr(project, {}) result = resolve_mr(project, {})
...@@ -40,24 +43,34 @@ RSpec.describe Resolvers::MergeRequestsResolver do ...@@ -40,24 +43,34 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end end
context 'by iid alone' do context 'by iid alone' do
it 'batch-resolves by target project full path and individual IID' do it 'batch-resolves by target project full path and individual IID', :request_store do
result = batch_sync(max_queries: 2) do # 1 query for project_authorizations, and 1 for merge_requests
result = batch_sync(max_queries: queries_per_project) do
[iid_1, iid_2].map { |iid| resolve_mr_single(project, iid) } [iid_1, iid_2].map { |iid| resolve_mr_single(project, iid) }
end end
expect(result).to contain_exactly(merge_request_1, merge_request_2) expect(result).to contain_exactly(merge_request_1, merge_request_2)
end end
it 'batch-resolves by target project full path and IIDS' do it 'batch-resolves by target project full path and IIDS', :request_store do
result = batch_sync(max_queries: 2) do result = batch_sync(max_queries: queries_per_project) do
resolve_mr(project, iids: [iid_1, iid_2]) resolve_mr(project, iids: [iid_1, iid_2])
end end
expect(result).to contain_exactly(merge_request_1, merge_request_2) expect(result).to contain_exactly(merge_request_1, merge_request_2)
end end
it 'batch-resolves by target project full path and IIDS, single or plural', :request_store do
result = batch_sync(max_queries: queries_per_project) do
[resolve_mr_single(project, merge_request_3.iid), *resolve_mr(project, iids: [iid_1, iid_2])]
end
expect(result).to contain_exactly(merge_request_1, merge_request_2, merge_request_3)
end
it 'can batch-resolve merge requests from different projects' do it 'can batch-resolve merge requests from different projects' do
result = batch_sync(max_queries: 3) do # 2 queries for project_authorizations, and 2 for merge_requests
result = batch_sync(max_queries: queries_per_project * 2) do
resolve_mr(project, iids: iid_1) + resolve_mr(project, iids: iid_1) +
resolve_mr(project, iids: iid_2) + resolve_mr(project, iids: iid_2) +
resolve_mr(other_project, iids: other_iid) resolve_mr(other_project, iids: other_iid)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Graphql::Loaders::IssuableLoader do
subject { described_class.new(parent, finder) }
let(:params) { HashWithIndifferentAccess.new }
describe '#find_all' do
let(:finder) { double(:finder, params: params, execute: %i[x y z]) }
where(:factory, :param_name) do
%i[project group].map { |thing| [thing, :"#{thing}_id"] }
end
with_them do
let(:parent) { build_stubbed(factory) }
it 'assignes the parent parameter, and batching_find_alls the finder' do
expect(subject.find_all).to contain_exactly(:x, :y, :z)
expect(params).to include(param_name => parent)
end
end
context 'the parent is of an unexpected type' do
let(:parent) { build(:merge_request) }
it 'raises an error if we pass an unexpected parent' do
expect { subject.find_all }.to raise_error(/Unexpected parent/)
end
end
end
describe '#batching_find_all' do
context 'the finder params are anything other than [iids]' do
let(:finder) { double(:finder, params: params, execute: [:foo]) }
let(:parent) { build_stubbed(:project) }
it 'batching_find_alls the finder, setting the correct parent parameter' do
expect(subject.batching_find_all).to eq([:foo])
expect(params[:project_id]).to eq(parent)
end
it 'allows a post-process block' do
expect(subject.batching_find_all(&:first)).to eq(:foo)
end
end
context 'the finder params are exactly [iids]' do
# Dumb finder class, that only implements what we need, and has
# predictable query counts.
let(:finder_class) do
Class.new do
attr_reader :current_user, :params
def initialize(user, args)
@current_user = user
@params = HashWithIndifferentAccess.new(args.to_h)
end
def execute
params[:project_id].issues.where(iid: params[:iids])
end
end
end
it 'batches requests' do
issue_a = create(:issue)
issue_b = create(:issue)
issue_c = create(:issue, project: issue_a.project)
proj_1 = issue_a.project
proj_2 = issue_b.project
user = create(:user, developer_projects: [proj_1, proj_2])
finder_a = finder_class.new(user, iids: [issue_a.iid])
finder_b = finder_class.new(user, iids: [issue_b.iid])
finder_c = finder_class.new(user, iids: [issue_c.iid])
results = []
expect do
results.concat(described_class.new(proj_1, finder_a).batching_find_all)
results.concat(described_class.new(proj_2, finder_b).batching_find_all)
results.concat(described_class.new(proj_1, finder_c).batching_find_all)
end.not_to exceed_query_limit(0)
expect do
results = results.map(&:sync)
end.not_to exceed_query_limit(2)
expect(results).to contain_exactly(issue_a, issue_b, issue_c)
end
end
end
end
...@@ -154,7 +154,7 @@ RSpec.describe GroupPolicy do ...@@ -154,7 +154,7 @@ RSpec.describe GroupPolicy do
context 'admin' do context 'admin' do
let(:current_user) { admin } let(:current_user) { admin }
it do specify do
expect_allowed(*read_group_permissions) expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions) expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions) expect_allowed(*reporter_permissions)
...@@ -162,6 +162,10 @@ RSpec.describe GroupPolicy do ...@@ -162,6 +162,10 @@ RSpec.describe GroupPolicy do
expect_allowed(*maintainer_permissions) expect_allowed(*maintainer_permissions)
expect_allowed(*owner_permissions) expect_allowed(*owner_permissions)
end end
context 'with admin mode', :enable_admin_mode do
specify { expect_allowed(*admin_permissions) }
end
end end
describe 'private nested group use the highest access level from the group and inherited permissions' do describe 'private nested group use the highest access level from the group and inherited permissions' do
......
...@@ -30,7 +30,7 @@ RSpec.describe ProjectPolicy do ...@@ -30,7 +30,7 @@ RSpec.describe ProjectPolicy do
admin_issue admin_label admin_list read_commit_status read_build admin_issue admin_label admin_list read_commit_status read_build
read_container_image read_pipeline read_environment read_deployment read_container_image read_pipeline read_environment read_deployment
read_merge_request download_wiki_code read_sentry_issue read_metrics_dashboard_annotation read_merge_request download_wiki_code read_sentry_issue read_metrics_dashboard_annotation
metrics_dashboard metrics_dashboard read_confidential_issues
] ]
end end
......
...@@ -38,6 +38,7 @@ RSpec.shared_context 'GroupPolicy context' do ...@@ -38,6 +38,7 @@ RSpec.shared_context 'GroupPolicy context' do
:update_default_branch_protection :update_default_branch_protection
].compact ].compact
end end
let(:admin_permissions) { %i[read_confidential_issues] }
before_all do before_all do
group.add_guest(guest) group.add_guest(guest)
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'resolving an issuable in GraphQL' do |type| RSpec.shared_examples 'resolving an issuable in GraphQL' do |type|
subject { mutation.resolve_issuable(type: type, parent_path: parent.full_path, iid: issuable.iid) } include GraphqlHelpers
let(:parent_path) { parent.full_path }
let(:iid) { issuable.iid }
subject(:result) { mutation.resolve_issuable(type: type, parent_path: parent_path, iid: iid) }
context 'when user has access' do context 'when user has access' do
before do before do
...@@ -9,37 +14,23 @@ RSpec.shared_examples 'resolving an issuable in GraphQL' do |type| ...@@ -9,37 +14,23 @@ RSpec.shared_examples 'resolving an issuable in GraphQL' do |type|
end end
it 'resolves issuable by iid' do it 'resolves issuable by iid' do
result = type == :merge_request ? subject.sync : subject
expect(result).to eq(issuable) expect(result).to eq(issuable)
end end
it 'uses the correct Resolver to resolve issuable' do context 'the IID does not refer to a valid issuable' do
resolver_class = "Resolvers::#{type.to_s.classify.pluralize}Resolver".constantize let(:iid) { '100' }
resolve_method = type == :epic ? :resolve_group : :resolve_project
resolved_parent = mutation.send(resolve_method, full_path: parent.full_path)
allow(mutation).to receive(resolve_method)
.with(full_path: parent.full_path)
.and_return(resolved_parent)
expect(resolver_class.single).to receive(:new)
.with(object: resolved_parent, context: context, field: nil)
.and_call_original
subject
end
it 'returns nil if issuable is not found' do
result = mutation.resolve_issuable(type: type, parent_path: parent.full_path, iid: "100")
result = result.respond_to?(:sync) ? result.sync : result
expect(result).to be_nil it 'returns nil' do
expect(result).to be_nil
end
end end
it 'returns nil if parent path is not present' do context 'the parent path is not present' do
result = mutation.resolve_issuable(type: type, parent_path: "", iid: issuable.iid) let(:parent_path) { '' }
expect(result).to be_nil it 'returns nil' do
expect(result).to be_nil
end
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment