Commit 4f0fcdf2 authored by charlie ablett's avatar charlie ablett Committed by Luke Duncalfe

Add type and type tests to GraphQL resolvers

- Add type declaration
- Add field declarations tests (including lookahead)
Co-authored-by: default avatarAlex Kalderimis <alex.kalderimis@gmail.com>
parent 7a752db3
...@@ -5,6 +5,8 @@ module Resolvers ...@@ -5,6 +5,8 @@ module Resolvers
class IntegrationsResolver < BaseResolver class IntegrationsResolver < BaseResolver
alias_method :project, :synchronized_object alias_method :project, :synchronized_object
type Types::AlertManagement::IntegrationType.connection_type, null: true
def resolve(**args) def resolve(**args)
return [] unless Feature.enabled?(:multiple_http_integrations, project) return [] unless Feature.enabled?(:multiple_http_integrations, project)
......
...@@ -8,32 +8,81 @@ module Resolvers ...@@ -8,32 +8,81 @@ module Resolvers
argument_class ::Types::BaseArgument argument_class ::Types::BaseArgument
def self.single def self.singular_type
@single ||= Class.new(self) do return unless type
def ready?(**args)
ready, early_return = super
[ready, select_result(early_return)]
end
def resolve(**args) unwrapped = type.unwrap
select_result(super)
end %i[node_type relay_node_type of_type itself].reduce(nil) do |t, m|
t || unwrapped.try(m)
end
end
def single? def self.when_single(&block)
true as_single << block
# Have we been called after defining the single version of this resolver?
if @single.present?
@single.instance_exec(&block)
end
end
def self.as_single
@as_single ||= []
end
def self.single_definition_blocks
ancestors.flat_map { |klass| klass.try(:as_single) || [] }
end
def self.single
@single ||= begin
parent = self
klass = Class.new(self) do
type parent.singular_type, null: true
def ready?(**args)
ready, early_return = super
[ready, select_result(early_return)]
end
def resolve(**args)
select_result(super)
end
def single?
true
end
def select_result(results)
results&.first
end
define_singleton_method :to_s do
"#{parent}.single"
end
end end
def select_result(results) single_definition_blocks.each do |definition|
results&.first klass.instance_exec(&definition)
end end
klass
end end
end end
def self.last def self.last
parent = self
@last ||= Class.new(self.single) do @last ||= Class.new(self.single) do
type parent.singular_type, null: true
def select_result(results) def select_result(results)
results&.last results&.last
end end
define_singleton_method :to_s do
"#{parent}.last"
end
end end
end end
...@@ -68,14 +117,13 @@ module Resolvers ...@@ -68,14 +117,13 @@ module Resolvers
end end
end end
# TODO: remove! This should never be necessary
# Remove as part of https://gitlab.com/gitlab-org/gitlab/-/issues/13984,
# since once we use that authorization approach, the object is guaranteed to
# be synchronized before any field.
def synchronized_object def synchronized_object
strong_memoize(:synchronized_object) do strong_memoize(:synchronized_object) do
case object ::Gitlab::Graphql::Lazy.force(object)
when BatchLoader::GraphQL
object.sync
else
object
end
end end
end end
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Resolvers module Resolvers
module DesignManagement module DesignManagement
class DesignResolver < BaseResolver class DesignResolver < BaseResolver
type ::Types::DesignManagement::DesignType, null: true
argument :id, ::Types::GlobalIDType[::DesignManagement::Design], argument :id, ::Types::GlobalIDType[::DesignManagement::Design],
required: false, required: false,
description: 'Find a design by its ID' description: 'Find a design by its ID'
......
...@@ -6,6 +6,8 @@ module Resolvers ...@@ -6,6 +6,8 @@ module Resolvers
DesignID = ::Types::GlobalIDType[::DesignManagement::Design] DesignID = ::Types::GlobalIDType[::DesignManagement::Design]
VersionID = ::Types::GlobalIDType[::DesignManagement::Version] VersionID = ::Types::GlobalIDType[::DesignManagement::Version]
type ::Types::DesignManagement::DesignType.connection_type, null: true
argument :ids, [DesignID], argument :ids, [DesignID],
required: false, required: false,
description: 'Filters designs by their ID' description: 'Filters designs by their ID'
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Resolvers module Resolvers
module ErrorTracking module ErrorTracking
class SentryDetailedErrorResolver < BaseResolver class SentryDetailedErrorResolver < BaseResolver
type Types::ErrorTracking::SentryDetailedErrorType, null: true
argument :id, GraphQL::ID_TYPE, argument :id, GraphQL::ID_TYPE,
required: true, required: true,
description: 'ID of the Sentry issue' description: 'ID of the Sentry issue'
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Resolvers module Resolvers
module ErrorTracking module ErrorTracking
class SentryErrorCollectionResolver < BaseResolver class SentryErrorCollectionResolver < BaseResolver
type Types::ErrorTracking::SentryErrorCollectionType, null: true
def resolve(**args) def resolve(**args)
project = object project = object
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Resolvers module Resolvers
module ErrorTracking module ErrorTracking
class SentryErrorsResolver < BaseResolver class SentryErrorsResolver < BaseResolver
type Types::ErrorTracking::SentryErrorType.connection_type, null: true
def resolve(**args) def resolve(**args)
args[:cursor] = args.delete(:after) args[:cursor] = args.delete(:after)
project = object.project project = object.project
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module Resolvers module Resolvers
class GroupMembersResolver < MembersResolver class GroupMembersResolver < MembersResolver
type Types::GroupMemberType.connection_type, null: true
authorize :read_group_member authorize :read_group_member
private private
......
...@@ -12,7 +12,7 @@ module Resolvers ...@@ -12,7 +12,7 @@ module Resolvers
required: false, required: false,
default_value: 'created_desc' default_value: 'created_desc'
type Types::IssueType, null: true type Types::IssueType.connection_type, null: true
NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc
label_priority_asc label_priority_desc label_priority_asc label_priority_desc
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module Resolvers module Resolvers
class ProjectPipelineResolver < BaseResolver class ProjectPipelineResolver < BaseResolver
type ::Types::Ci::PipelineType, null: true
alias_method :project, :object alias_method :project, :object
argument :iid, GraphQL::ID_TYPE, argument :iid, GraphQL::ID_TYPE,
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Resolvers module Resolvers
module Projects module Projects
class JiraImportsResolver < BaseResolver class JiraImportsResolver < BaseResolver
type Types::JiraImportType.connection_type, null: true
include Gitlab::Graphql::Authorize::AuthorizeResource include Gitlab::Graphql::Authorize::AuthorizeResource
alias_method :project, :object alias_method :project, :object
......
...@@ -5,6 +5,8 @@ module Resolvers ...@@ -5,6 +5,8 @@ module Resolvers
class JiraProjectsResolver < BaseResolver class JiraProjectsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Projects::Services::JiraProjectType.connection_type, null: true
argument :name, argument :name,
GraphQL::STRING_TYPE, GraphQL::STRING_TYPE,
required: false, required: false,
......
...@@ -5,6 +5,8 @@ module Resolvers ...@@ -5,6 +5,8 @@ module Resolvers
class ServicesResolver < BaseResolver class ServicesResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Projects::ServiceType.connection_type, null: true
argument :active, argument :active,
GraphQL::BOOLEAN_TYPE, GraphQL::BOOLEAN_TYPE,
required: false, required: false,
......
...@@ -5,6 +5,8 @@ module Resolvers ...@@ -5,6 +5,8 @@ module Resolvers
class BlobsResolver < BaseResolver class BlobsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Snippets::BlobType.connection_type, null: true
alias_method :snippet, :object alias_method :snippet, :object
argument :paths, [GraphQL::STRING_TYPE], argument :paths, [GraphQL::STRING_TYPE],
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Resolvers module Resolvers
class TodoResolver < BaseResolver class TodoResolver < BaseResolver
type Types::TodoType, null: true type Types::TodoType.connection_type, null: true
alias_method :target, :object alias_method :target, :object
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module Resolvers module Resolvers
class TreeResolver < BaseResolver class TreeResolver < BaseResolver
type Types::Tree::TreeType, null: true
argument :path, GraphQL::STRING_TYPE, argument :path, GraphQL::STRING_TYPE,
required: false, required: false,
default_value: '', default_value: '',
......
...@@ -4,6 +4,7 @@ module Resolvers ...@@ -4,6 +4,7 @@ module Resolvers
class UsersResolver < BaseResolver class UsersResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::UserType.connection_type, null: true
description 'Find Users' description 'Find Users'
argument :ids, [GraphQL::ID_TYPE], argument :ids, [GraphQL::ID_TYPE],
......
...@@ -122,7 +122,7 @@ module EE ...@@ -122,7 +122,7 @@ module EE
::Types::Clusters::AgentType, ::Types::Clusters::AgentType,
null: true, null: true,
description: 'Find a single cluster agent by name', description: 'Find a single cluster agent by name',
resolver: ::Resolvers::Clusters::AgentResolver.single resolver: ::Resolvers::Clusters::AgentsResolver.single
field :cluster_agents, field :cluster_agents,
::Types::Clusters::AgentType.connection_type, ::Types::Clusters::AgentType.connection_type,
......
# frozen_string_literal: true
module Resolvers
module Clusters
class AgentResolver < AgentsResolver
argument :name, GraphQL::STRING_TYPE,
required: true,
description: 'Name of the cluster agent'
end
end
end
...@@ -5,14 +5,22 @@ module Resolvers ...@@ -5,14 +5,22 @@ module Resolvers
class AgentsResolver < BaseResolver class AgentsResolver < BaseResolver
include LooksAhead include LooksAhead
type Types::Clusters::AgentType, null: true type Types::Clusters::AgentType.connection_type, null: true
extras [:lookahead]
when_single do
argument :name, GraphQL::STRING_TYPE,
required: true,
description: 'Name of the cluster agent'
end
alias_method :project, :object alias_method :project, :object
def resolve_with_lookahead(**args) def resolve_with_lookahead(**args)
apply_lookahead( apply_lookahead(
::Clusters::AgentsFinder ::Clusters::AgentsFinder
.new(project, context[:current_user], params: args) .new(project, current_user, params: args)
.execute .execute
) )
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Resolvers module Resolvers
class EpicIssuesResolver < BaseResolver class EpicIssuesResolver < BaseResolver
type Types::EpicIssueType, null: true type Types::EpicIssueType.connection_type, null: true
alias_method :epic, :object alias_method :epic, :object
......
...@@ -5,6 +5,8 @@ module Resolvers ...@@ -5,6 +5,8 @@ module Resolvers
class RequirementsResolver < BaseResolver class RequirementsResolver < BaseResolver
include LooksAhead include LooksAhead
type ::Types::RequirementsManagement::RequirementType.connection_type, null: true
argument :iid, GraphQL::ID_TYPE, argument :iid, GraphQL::ID_TYPE,
required: false, required: false,
description: 'IID of the requirement, e.g., "1"' description: 'IID of the requirement, e.g., "1"'
...@@ -29,8 +31,6 @@ module Resolvers ...@@ -29,8 +31,6 @@ module Resolvers
required: false, required: false,
description: 'Filter requirements by author username' description: 'Filter requirements by author username'
type Types::RequirementsManagement::RequirementType, null: true
def resolve_with_lookahead(**args) def resolve_with_lookahead(**args)
# The project could have been loaded in batch by `BatchLoader`. # The project could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project to query for issues, so # At this point we need the `id` of the project to query for issues, so
......
...@@ -4,6 +4,8 @@ module Resolvers ...@@ -4,6 +4,8 @@ module Resolvers
class TimelogResolver < BaseResolver class TimelogResolver < BaseResolver
include LooksAhead include LooksAhead
type ::Types::TimelogType.connection_type, null: false
argument :start_date, Types::TimeType, argument :start_date, Types::TimeType,
required: false, required: false,
description: 'List time logs within a date range where the logged date is equal to or after startDate' description: 'List time logs within a date range where the logged date is equal to or after startDate'
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Clusters::AgentResolver do
it { expect(described_class).to be < Resolvers::Clusters::AgentsResolver }
describe 'arguments' do
subject { described_class.arguments[argument] }
describe 'name' do
let(:argument) { 'name' }
it do
expect(subject).to be_present
expect(subject.type.to_s).to eq('String!')
expect(subject.description).to be_present
end
end
end
end
...@@ -5,10 +5,13 @@ require 'spec_helper' ...@@ -5,10 +5,13 @@ require 'spec_helper'
RSpec.describe Resolvers::Clusters::AgentsResolver do RSpec.describe Resolvers::Clusters::AgentsResolver do
include GraphqlHelpers include GraphqlHelpers
it { expect(described_class).to be < LooksAhead } specify do
expect(described_class).to have_nullable_graphql_type(Types::Clusters::AgentType.connection_type)
end
it { expect(described_class.type).to eq(Types::Clusters::AgentType) } specify do
it { expect(described_class.null).to be_truthy } expect(described_class.field_options).to include(extras: include(:lookahead))
end
describe '#resolve' do describe '#resolve' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
...@@ -38,3 +41,33 @@ RSpec.describe Resolvers::Clusters::AgentsResolver do ...@@ -38,3 +41,33 @@ RSpec.describe Resolvers::Clusters::AgentsResolver do
end end
end end
end end
RSpec.describe Resolvers::Clusters::AgentsResolver.single do
it { expect(described_class).to be < Resolvers::Clusters::AgentsResolver }
describe '.field_options' do
subject { described_class.field_options }
specify do
expect(subject).to include(
type: ::Types::Clusters::AgentType,
null: true,
extras: [:lookahead]
)
end
end
describe 'arguments' do
subject { described_class.arguments[argument] }
describe 'name' do
let(:argument) { 'name' }
it do
expect(subject).to be_present
expect(subject.type.to_s).to eq('String!')
expect(subject.description).to be_present
end
end
end
end
...@@ -21,6 +21,10 @@ RSpec.describe Resolvers::EpicIssuesResolver do ...@@ -21,6 +21,10 @@ RSpec.describe Resolvers::EpicIssuesResolver do
let_it_be(:epic_issue3) { create(:epic_issue, epic: epic2, issue: issue3, relative_position: 1) } let_it_be(:epic_issue3) { create(:epic_issue, epic: epic2, issue: issue3, relative_position: 1) }
let_it_be(:epic_issue4) { create(:epic_issue, epic: epic2, issue: issue4, relative_position: nil) } let_it_be(:epic_issue4) { create(:epic_issue, epic: epic2, issue: issue4, relative_position: nil) }
specify do
expect(described_class).to have_nullable_graphql_type(Types::EpicIssueType.connection_type)
end
before do before do
group.add_developer(current_user) group.add_developer(current_user)
stub_licensed_features(epics: true) stub_licensed_features(epics: true)
......
...@@ -10,6 +10,10 @@ RSpec.describe Resolvers::RequirementsManagement::RequirementsResolver do ...@@ -10,6 +10,10 @@ RSpec.describe Resolvers::RequirementsManagement::RequirementsResolver do
let_it_be(:third_user) { create(:user) } let_it_be(:third_user) { create(:user) }
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
specify do
expect(described_class).to have_nullable_graphql_type(::Types::RequirementsManagement::RequirementType.connection_type)
end
context 'with a project' do context 'with a project' do
let_it_be(:requirement1) { create(:requirement, project: project, state: :opened, created_at: 5.hours.ago, title: 'it needs to do the thing', author: current_user) } let_it_be(:requirement1) { create(:requirement, project: project, state: :opened, created_at: 5.hours.ago, title: 'it needs to do the thing', author: current_user) }
let_it_be(:requirement2) { create(:requirement, project: project, state: :archived, created_at: 3.hours.ago, title: 'it needs to not break', author: other_user) } let_it_be(:requirement2) { create(:requirement, project: project, state: :archived, created_at: 3.hours.ago, title: 'it needs to not break', author: other_user) }
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Resolvers::TimelogResolver do RSpec.describe Resolvers::TimelogResolver do
include GraphqlHelpers include GraphqlHelpers
specify do
expect(described_class).to have_non_null_graphql_type(::Types::TimelogType.connection_type)
end
context "within a group" do context "within a group" do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
......
...@@ -14,6 +14,10 @@ RSpec.describe Resolvers::AlertManagement::IntegrationsResolver do ...@@ -14,6 +14,10 @@ RSpec.describe Resolvers::AlertManagement::IntegrationsResolver do
subject { sync(resolve_http_integrations) } subject { sync(resolve_http_integrations) }
specify do
expect(described_class).to have_nullable_graphql_type(Types::AlertManagement::IntegrationType.connection_type)
end
context 'user does not have permission' do context 'user does not have permission' do
it { is_expected.to be_empty } it { is_expected.to be_empty }
end end
......
...@@ -7,10 +7,13 @@ RSpec.describe Resolvers::BaseResolver do ...@@ -7,10 +7,13 @@ RSpec.describe Resolvers::BaseResolver do
let(:resolver) do let(:resolver) do
Class.new(described_class) do Class.new(described_class) do
def resolve(**args) argument :test, ::GraphQL::INT_TYPE, required: false
type [::GraphQL::INT_TYPE], null: true
def resolve(test: 100)
process(object) process(object)
[args, args] [test, test]
end end
def process(obj); end def process(obj); end
...@@ -19,17 +22,75 @@ RSpec.describe Resolvers::BaseResolver do ...@@ -19,17 +22,75 @@ RSpec.describe Resolvers::BaseResolver do
let(:last_resolver) do let(:last_resolver) do
Class.new(described_class) do Class.new(described_class) do
type [::GraphQL::INT_TYPE], null: true
def resolve(**args) def resolve(**args)
[1, 2] [1, 2]
end end
end end
end end
describe '.singular_type' do
subject { resolver.singular_type }
context 'for a connection of scalars' do
let(:resolver) do
Class.new(described_class) do
type ::GraphQL::INT_TYPE.connection_type, null: true
end
end
it { is_expected.to eq(::GraphQL::INT_TYPE) }
end
context 'for a connection of objects' do
let(:object) do
Class.new(::Types::BaseObject) do
graphql_name 'Foo'
end
end
let(:resolver) do
conn = object.connection_type
Class.new(described_class) do
type conn, null: true
end
end
it { is_expected.to eq(object) }
end
context 'for a list type' do
let(:resolver) do
Class.new(described_class) do
type [::GraphQL::STRING_TYPE], null: true
end
end
it { is_expected.to eq(::GraphQL::STRING_TYPE) }
end
context 'for a scalar type' do
let(:resolver) do
Class.new(described_class) do
type ::GraphQL::BOOLEAN_TYPE, null: true
end
end
it { is_expected.to eq(::GraphQL::BOOLEAN_TYPE) }
end
end
describe '.single' do describe '.single' do
it 'returns a subclass from the resolver' do it 'returns a subclass from the resolver' do
expect(resolver.single.superclass).to eq(resolver) expect(resolver.single.superclass).to eq(resolver)
end end
it 'has the correct (singular) type' do
expect(resolver.single.type).to eq(::GraphQL::INT_TYPE)
end
it 'returns the same subclass every time' do it 'returns the same subclass every time' do
expect(resolver.single.object_id).to eq(resolver.single.object_id) expect(resolver.single.object_id).to eq(resolver.single.object_id)
end end
...@@ -37,15 +98,106 @@ RSpec.describe Resolvers::BaseResolver do ...@@ -37,15 +98,106 @@ RSpec.describe Resolvers::BaseResolver do
it 'returns a resolver that gives the first result from the original resolver' do it 'returns a resolver that gives the first result from the original resolver' do
result = resolve(resolver.single, args: { test: 1 }) result = resolve(resolver.single, args: { test: 1 })
expect(result).to eq(test: 1) expect(result).to eq(1)
end
end
describe '.when_single' do
let(:resolver) do
Class.new(described_class) do
type [::GraphQL::INT_TYPE], null: true
when_single do
argument :foo, ::GraphQL::INT_TYPE, required: true
end
def resolve(foo: 1)
[foo * foo] # rubocop: disable Lint/BinaryOperatorWithIdenticalOperands
end
end
end
it 'does not apply the block to the resolver' do
expect(resolver.field_options).to include(
arguments: be_empty
)
result = resolve(resolver)
expect(result).to eq([1])
end
it 'applies the block to the single version of the resolver' do
expect(resolver.single.field_options).to include(
arguments: match('foo' => an_instance_of(::Types::BaseArgument))
)
result = resolve(resolver.single, args: { foo: 7 })
expect(result).to eq(49)
end
context 'multiple when_single blocks' do
let(:resolver) do
Class.new(described_class) do
type [::GraphQL::INT_TYPE], null: true
when_single do
argument :foo, ::GraphQL::INT_TYPE, required: true
end
when_single do
argument :bar, ::GraphQL::INT_TYPE, required: true
end
def resolve(foo: 1, bar: 2)
[foo * bar]
end
end
end
it 'applies both blocks to the single version of the resolver' do
expect(resolver.single.field_options).to include(
arguments: match('foo' => ::Types::BaseArgument, 'bar' => ::Types::BaseArgument)
)
result = resolve(resolver.single, args: { foo: 7, bar: 5 })
expect(result).to eq(35)
end
end
context 'inheritance' do
let(:subclass) do
Class.new(resolver) do
when_single do
argument :inc, ::GraphQL::INT_TYPE, required: true
end
def resolve(foo:, inc:)
super(foo: foo + inc)
end
end
end
it 'applies both blocks to the single version of the resolver' do
expect(resolver.single.field_options).to include(
arguments: match('foo' => ::Types::BaseArgument)
)
expect(subclass.single.field_options).to include(
arguments: match('foo' => ::Types::BaseArgument, 'inc' => ::Types::BaseArgument)
)
result = resolve(subclass.single, args: { foo: 7, inc: 1 })
expect(result).to eq(64)
end
end end
end end
context 'when the resolver returns early' do context 'when the resolver returns early' do
let(:resolver) do let(:resolver) do
Class.new(described_class) do Class.new(described_class) do
type [::GraphQL::STRING_TYPE], null: true
def ready?(**args) def ready?(**args)
[false, %w(early return)] [false, %w[early return]]
end end
def resolve(**args) def resolve(**args)
...@@ -121,28 +273,4 @@ RSpec.describe Resolvers::BaseResolver do ...@@ -121,28 +273,4 @@ RSpec.describe Resolvers::BaseResolver do
end end
end end
end end
describe '#synchronized_object' do
let(:object) { double(foo: :the_foo) }
let(:resolver) do
Class.new(described_class) do
def resolve(**args)
[synchronized_object.foo]
end
end
end
it 'handles raw objects' do
expect(resolve(resolver, obj: object)).to contain_exactly(:the_foo)
end
it 'handles lazy objects' do
delayed = BatchLoader::GraphQL.for(1).batch do |_, loader|
loader.call(1, object)
end
expect(resolve(resolver, obj: delayed)).to contain_exactly(:the_foo)
end
end
end end
...@@ -6,6 +6,10 @@ RSpec.describe Resolvers::DesignManagement::DesignResolver do ...@@ -6,6 +6,10 @@ RSpec.describe Resolvers::DesignManagement::DesignResolver do
include GraphqlHelpers include GraphqlHelpers
include DesignManagementTestHelpers include DesignManagementTestHelpers
specify do
expect(described_class).to have_nullable_graphql_type(::Types::DesignManagement::DesignType)
end
before do before do
enable_design_management enable_design_management
end end
......
...@@ -6,6 +6,10 @@ RSpec.describe Resolvers::DesignManagement::DesignsResolver do ...@@ -6,6 +6,10 @@ RSpec.describe Resolvers::DesignManagement::DesignsResolver do
include GraphqlHelpers include GraphqlHelpers
include DesignManagementTestHelpers include DesignManagementTestHelpers
specify do
expect(described_class).to have_nullable_graphql_type(::Types::DesignManagement::DesignType.connection_type)
end
before do before do
enable_design_management enable_design_management
end end
......
...@@ -9,10 +9,7 @@ RSpec.describe Resolvers::EchoResolver do ...@@ -9,10 +9,7 @@ RSpec.describe Resolvers::EchoResolver do
let(:text) { 'Message test' } let(:text) { 'Message test' }
specify do specify do
expect(described_class.field_options).to include( expect(described_class).to have_non_null_graphql_type(::GraphQL::STRING_TYPE)
type: eq(::GraphQL::STRING_TYPE),
null: be_falsey
)
end end
describe '#resolve' do describe '#resolve' do
......
...@@ -10,6 +10,10 @@ RSpec.describe Resolvers::ErrorTracking::SentryDetailedErrorResolver do ...@@ -10,6 +10,10 @@ RSpec.describe Resolvers::ErrorTracking::SentryDetailedErrorResolver do
let(:issue_details_service) { spy('ErrorTracking::IssueDetailsService') } let(:issue_details_service) { spy('ErrorTracking::IssueDetailsService') }
specify do
expect(described_class).to have_nullable_graphql_type(Types::ErrorTracking::SentryDetailedErrorType)
end
before do before do
project.add_developer(current_user) project.add_developer(current_user)
......
...@@ -10,6 +10,10 @@ RSpec.describe Resolvers::ErrorTracking::SentryErrorCollectionResolver do ...@@ -10,6 +10,10 @@ RSpec.describe Resolvers::ErrorTracking::SentryErrorCollectionResolver do
let(:list_issues_service) { spy('ErrorTracking::ListIssuesService') } let(:list_issues_service) { spy('ErrorTracking::ListIssuesService') }
specify do
expect(described_class).to have_nullable_graphql_type(Types::ErrorTracking::SentryErrorCollectionType)
end
before do before do
project.add_developer(current_user) project.add_developer(current_user)
......
...@@ -14,6 +14,10 @@ RSpec.describe Resolvers::ErrorTracking::SentryErrorsResolver do ...@@ -14,6 +14,10 @@ RSpec.describe Resolvers::ErrorTracking::SentryErrorsResolver do
let(:issues) { nil } let(:issues) { nil }
let(:pagination) { nil } let(:pagination) { nil }
specify do
expect(described_class).to have_nullable_graphql_type(Types::ErrorTracking::SentryErrorType.connection_type)
end
describe '#resolve' do describe '#resolve' do
context 'insufficient user permission' do context 'insufficient user permission' do
let(:user) { create(:user) } let(:user) { create(:user) }
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Resolvers::GroupMembersResolver do RSpec.describe Resolvers::GroupMembersResolver do
include GraphqlHelpers include GraphqlHelpers
specify do
expect(described_class).to have_nullable_graphql_type(Types::GroupMemberType.connection_type)
end
it_behaves_like 'querying members with a group' do it_behaves_like 'querying members with a group' do
let_it_be(:resource_member) { create(:group_member, user: user_1, group: group_1) } let_it_be(:resource_member) { create(:group_member, user: user_1, group: group_1) }
let_it_be(:resource) { group_1 } let_it_be(:resource) { group_1 }
......
...@@ -20,6 +20,10 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -20,6 +20,10 @@ RSpec.describe Resolvers::IssuesResolver do
let_it_be(:label1) { create(:label, project: project) } let_it_be(:label1) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) } let_it_be(:label2) { create(:label, project: project) }
specify do
expect(described_class).to have_nullable_graphql_type(Types::IssueType.connection_type)
end
context "with a project" do context "with a project" do
before do before do
project.add_developer(current_user) project.add_developer(current_user)
......
...@@ -10,6 +10,10 @@ RSpec.describe Resolvers::ProjectPipelineResolver do ...@@ -10,6 +10,10 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
let_it_be(:other_pipeline) { create(:ci_pipeline) } let_it_be(:other_pipeline) { create(:ci_pipeline) }
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
specify do
expect(described_class).to have_nullable_graphql_type(::Types::Ci::PipelineType)
end
def resolve_pipeline(project, args) def resolve_pipeline(project, args)
resolve(described_class, obj: project, args: args, ctx: { current_user: current_user }) resolve(described_class, obj: project, args: args, ctx: { current_user: current_user })
end end
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Resolvers::Projects::JiraImportsResolver do RSpec.describe Resolvers::Projects::JiraImportsResolver do
include GraphqlHelpers include GraphqlHelpers
specify do
expect(described_class).to have_nullable_graphql_type(Types::JiraImportType.connection_type)
end
describe '#resolve' do describe '#resolve' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public) } let_it_be(:project, reload: true) { create(:project, :public) }
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Resolvers::Projects::JiraProjectsResolver do RSpec.describe Resolvers::Projects::JiraProjectsResolver do
include GraphqlHelpers include GraphqlHelpers
specify do
expect(described_class).to have_nullable_graphql_type(Types::Projects::Services::JiraProjectType.connection_type)
end
describe '#resolve' do describe '#resolve' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Resolvers::Projects::ServicesResolver do RSpec.describe Resolvers::Projects::ServicesResolver do
include GraphqlHelpers include GraphqlHelpers
specify do
expect(described_class).to have_nullable_graphql_type(Types::Projects::ServiceType.connection_type)
end
describe '#resolve' do describe '#resolve' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Resolvers::Snippets::BlobsResolver do RSpec.describe Resolvers::Snippets::BlobsResolver do
include GraphqlHelpers include GraphqlHelpers
specify do
expect(described_class).to have_nullable_graphql_type(Types::Snippets::BlobType.connection_type)
end
describe '#resolve' do describe '#resolve' do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be(:snippet) { create(:personal_snippet, :private, :repository, author: current_user) } let_it_be(:snippet) { create(:personal_snippet, :private, :repository, author: current_user) }
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Resolvers::TodoResolver do RSpec.describe Resolvers::TodoResolver do
include GraphqlHelpers include GraphqlHelpers
specify do
expect(described_class).to have_nullable_graphql_type(Types::TodoType.connection_type)
end
describe '#resolve' do describe '#resolve' do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be(:author1) { create(:user) } let_it_be(:author1) { create(:user) }
......
...@@ -7,6 +7,10 @@ RSpec.describe Resolvers::TreeResolver do ...@@ -7,6 +7,10 @@ RSpec.describe Resolvers::TreeResolver do
let(:repository) { create(:project, :repository).repository } let(:repository) { create(:project, :repository).repository }
specify do
expect(described_class).to have_nullable_graphql_type(Types::Tree::TreeType)
end
describe '#resolve' do describe '#resolve' do
it 'resolves to a tree' do it 'resolves to a tree' do
result = resolve_repository({ ref: "master" }) result = resolve_repository({ ref: "master" })
......
...@@ -8,6 +8,10 @@ RSpec.describe Resolvers::UsersResolver do ...@@ -8,6 +8,10 @@ RSpec.describe Resolvers::UsersResolver do
let_it_be(:user1) { create(:user, name: "SomePerson") } let_it_be(:user1) { create(:user, name: "SomePerson") }
let_it_be(:user2) { create(:user, username: "someone123784") } let_it_be(:user2) { create(:user, username: "someone123784") }
specify do
expect(described_class).to have_nullable_graphql_type(Types::UserType.connection_type)
end
describe '#resolve' do describe '#resolve' do
it 'raises an error when read_users_list is not authorized' do it 'raises an error when read_users_list is not authorized' do
expect(Ability).to receive(:allowed?).with(nil, :read_users_list).and_return(false) expect(Ability).to receive(:allowed?).with(nil, :read_users_list).and_return(false)
......
...@@ -109,15 +109,82 @@ RSpec::Matchers.define :have_graphql_arguments do |*expected| ...@@ -109,15 +109,82 @@ RSpec::Matchers.define :have_graphql_arguments do |*expected|
end end
end end
RSpec::Matchers.define :have_graphql_type do |expected| module GraphQLTypeHelpers
match do |field| def message(object, expected, **opts)
expect(field.type).to eq(expected) non_null = expected.non_null? || (opts.key?(:null) && !opts[:null])
actual = object.type
actual_type = actual.unwrap.graphql_name
actual_type += '!' if actual.non_null?
expected_type = expected.unwrap.graphql_name
expected_type += '!' if non_null
"expected #{describe_object(object)} to have GraphQL type #{expected_type}, but got #{actual_type}"
end
def describe_object(object)
case object
when Types::BaseField
"#{describe_object(object.owner_type)}.#{object.graphql_name}"
when Types::BaseArgument
"#{describe_object(object.owner)}.#{object.graphql_name}"
when Class
object.try(:graphql_name) || object.name
else
object.to_s
end
end
def nullified(type, can_be_nil)
return type if can_be_nil.nil? # unknown!
return type if can_be_nil
type.to_non_null_type
end
end
RSpec::Matchers.define :have_graphql_type do |expected, opts = {}|
include GraphQLTypeHelpers
match do |object|
expect(object.type).to eq(nullified(expected, opts[:null]))
end
failure_message do |object|
message(object, expected, **opts)
end
end
RSpec::Matchers.define :have_nullable_graphql_type do |expected|
include GraphQLTypeHelpers
match do |object|
expect(object).to have_graphql_type(expected.unwrap, { null: true })
end
description do
"have nullable GraphQL type #{expected.graphql_name}"
end
failure_message do |object|
message(object, expected, null: true)
end end
end end
RSpec::Matchers.define :have_non_null_graphql_type do |expected| RSpec::Matchers.define :have_non_null_graphql_type do |expected|
match do |field| include GraphQLTypeHelpers
expect(field.type.to_graphql).to eq(!expected.to_graphql)
match do |object|
expect(object).to have_graphql_type(expected, { null: false })
end
description do
"have non-null GraphQL type #{expected.graphql_name}"
end
failure_message do |object|
message(object, expected, null: false)
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