Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support/helpers/graphql_helpers.rb')
-rw-r--r--spec/support/helpers/graphql_helpers.rb155
1 files changed, 109 insertions, 46 deletions
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index db8d45f61ea..d0a1941817a 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -26,10 +26,44 @@ module GraphqlHelpers
end
end
+ # Some arguments use `as:` to expose a different name internally.
+ # Transform the args to use those names
+ def self.deep_transform_args(args, field)
+ args.to_h do |k, v|
+ argument = field.arguments[k.to_s.camelize(:lower)]
+ [argument.keyword, v.is_a?(Hash) ? deep_transform_args(v, argument.type) : v]
+ end
+ end
+
+ # Convert incoming args into the form usually passed in from the client,
+ # all strings, etc.
+ def self.as_graphql_argument_literals(args)
+ args.transform_values { |value| transform_arg_value(value) }
+ end
+
+ def self.transform_arg_value(value)
+ case value
+ when Hash
+ as_graphql_argument_literals(value)
+ when Array
+ value.map { |x| transform_arg_value(x) }
+ when Time, ActiveSupport::TimeWithZone
+ value.strftime("%F %T.%N %z")
+ when Date, GlobalID, Symbol
+ value.to_s
+ else
+ value
+ end
+ end
+
# Run this resolver exactly as it would be called in the framework. This
# includes all authorization hooks, all argument processing and all result
# wrapping.
# see: GraphqlHelpers#resolve_field
+ #
+ # TODO: this is too coupled to gem internals, making upgrades incredibly
+ # painful, and bypasses much of the validation of the framework.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/363121
def resolve(
resolver_class, # [Class[<= BaseResolver]] The resolver at test.
obj: nil, # [Any] The BaseObject#object for the resolver (available as `#object` in the resolver).
@@ -37,7 +71,8 @@ module GraphqlHelpers
ctx: {}, # [#to_h] The current context values.
schema: GitlabSchema, # [GraphQL::Schema] Schema to use during execution.
parent: :not_given, # A GraphQL query node to be passed as the `:parent` extra.
- lookahead: :not_given # A GraphQL lookahead object to be passed as the `:lookahead` extra.
+ lookahead: :not_given, # A GraphQL lookahead object to be passed as the `:lookahead` extra.
+ arg_style: :internal_prepared # Args are in internal format, but should use more rigorous processing
)
# All resolution goes through fields, so we need to create one here that
# uses our resolver. Thankfully, apart from the field name, resolvers
@@ -49,7 +84,6 @@ module GraphqlHelpers
field = ::Types::BaseField.new(**field_options)
# All mutations accept a single `:input` argument. Wrap arguments here.
- # See the unwrapping below in GraphqlHelpers#resolve_field
args = { input: args } if resolver_class <= ::Mutations::BaseMutation && !args.key?(:input)
resolve_field(field, obj,
@@ -57,7 +91,8 @@ module GraphqlHelpers
ctx: ctx,
schema: schema,
object_type: resolver_parent,
- extras: { parent: parent, lookahead: lookahead })
+ extras: { parent: parent, lookahead: lookahead },
+ arg_style: arg_style)
end
# Resolve the value of a field on an object.
@@ -85,21 +120,22 @@ module GraphqlHelpers
# NB: Arguments are passed from the client's perspective. If there is an argument
# `foo` aliased as `bar`, then we would pass `args: { bar: the_value }`, and
# types are checked before resolution.
+ # rubocop:disable Metrics/ParameterLists
def resolve_field(
- field, # An instance of `BaseField`, or the name of a field on the current described_class
- object, # The current object of the `BaseObject` this field 'belongs' to
- args: {}, # Field arguments (keys will be fieldnamerized)
- ctx: {}, # Context values (important ones are :current_user)
- extras: {}, # Stub values for field extras (parent and lookahead)
- current_user: :not_given, # The current user (specified explicitly, overrides ctx[:current_user])
- schema: GitlabSchema, # A specific schema instance
- object_type: described_class # The `BaseObject` type this field belongs to
+ field, # An instance of `BaseField`, or the name of a field on the current described_class
+ object, # The current object of the `BaseObject` this field 'belongs' to
+ args: {}, # Field arguments (keys will be fieldnamerized)
+ ctx: {}, # Context values (important ones are :current_user)
+ extras: {}, # Stub values for field extras (parent and lookahead)
+ current_user: :not_given, # The current user (specified explicitly, overrides ctx[:current_user])
+ schema: GitlabSchema, # A specific schema instance
+ object_type: described_class, # The `BaseObject` type this field belongs to
+ arg_style: :internal_prepared # Args are in internal format, but should use more rigorous processing
)
field = to_base_field(field, object_type)
ctx[:current_user] = current_user unless current_user == :not_given
query = GraphQL::Query.new(schema, context: ctx.to_h)
extras[:lookahead] = negative_lookahead if extras[:lookahead] == :not_given && field.extras.include?(:lookahead)
-
query_ctx = query.context
mock_extras(query_ctx, **extras)
@@ -107,29 +143,58 @@ module GraphqlHelpers
parent = object_type.authorized_new(object, query_ctx)
raise UnauthorizedObject unless parent
- # TODO: This will need to change when we move to the interpreter:
- # At that point, arguments will be a plain ruby hash rather than
- # an Arguments object
- # see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27536
- # https://gitlab.com/gitlab-org/gitlab/-/issues/210556
- arguments = field.to_graphql.arguments_class.new(
- GraphqlHelpers.deep_fieldnamerize(args),
- context: query_ctx,
- defaults_used: []
- )
-
# we enable the request store so we can track gitaly calls.
::Gitlab::WithRequestStore.with_request_store do
- # TODO: This will need to change when we move to the interpreter - at that
- # point we will call `field#resolve`
-
- # Unwrap the arguments to mutations. This pairs with the wrapping in GraphqlHelpers#resolve
- # If arguments are not wrapped first, then arguments processing will raise.
- # If arguments are not unwrapped here, then the resolve method of the mutation will raise argument errors.
- arguments = arguments.to_kwargs[:input] if field.resolver && field.resolver <= ::Mutations::BaseMutation
+ prepared_args = case arg_style
+ when :internal_prepared
+ args_internal_prepared(field, args: args, query_ctx: query_ctx, parent: parent, extras: extras, query: query)
+ else
+ args_internal(field, args: args, query_ctx: query_ctx, parent: parent, extras: extras, query: query)
+ end
+
+ if prepared_args.class <= Gitlab::Graphql::Errors::BaseError
+ prepared_args
+ else
+ field.resolve(parent, prepared_args, query_ctx)
+ end
+ end
+ end
+ # rubocop:enable Metrics/ParameterLists
- field.resolve_field(parent, arguments, query_ctx)
+ # Pros:
+ # - Original way we handled arguments
+ #
+ # Cons:
+ # - the `prepare` method of a type is not called. Whether as a proc or as a method
+ # on the type, it's not called. For example `:cluster_id` in ee/app/graphql/resolvers/vulnerabilities_resolver.rb,
+ # or `prepare` in app/graphql/types/range_input_type.rb, used by Types::TimeframeInputType
+ def args_internal(field, args:, query_ctx:, parent:, extras:, query:)
+ arguments = GraphqlHelpers.deep_transform_args(args, field)
+ arguments.merge!(extras.reject {|k, v| v == :not_given})
+ end
+
+ # Pros:
+ # - Allows the use of ruby types, without having to pass in strings
+ # - All args are converted into strings just like if it was called from a client
+ # - Much stronger argument verification
+ #
+ # Cons:
+ # - Some values, such as enums, would need to be changed in the specs to use the
+ # external values, because there is no easy way to handle them.
+ #
+ # take internal style args, and force them into client style args
+ def args_internal_prepared(field, args:, query_ctx:, parent:, extras:, query:)
+ arguments = GraphqlHelpers.as_graphql_argument_literals(args)
+ arguments.merge!(extras.reject {|k, v| v == :not_given})
+
+ # Use public API to properly prepare the args for use by the resolver.
+ # It uses `coerce_arguments` under the covers
+ prepared_args = nil
+ query.arguments_cache.dataload_for(GraphqlHelpers.deep_fieldnamerize(arguments), field, parent) do |kwarg_arguments|
+ prepared_args = kwarg_arguments
end
+
+ prepared_args.respond_to?(:keyword_arguments) ? prepared_args.keyword_arguments : prepared_args
end
def mock_extras(context, parent: :not_given, lookahead: :not_given)
@@ -148,7 +213,7 @@ module GraphqlHelpers
def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema, subscription_update: false)
if ctx.is_a?(Hash)
- q = double('Query', schema: schema, subscription_update?: subscription_update)
+ q = double('Query', schema: schema, subscription_update?: subscription_update, warden: GraphQL::Schema::Warden::PassThruWarden)
ctx = GraphQL::Query::Context.new(query: q, object: obj, values: ctx)
end
@@ -357,8 +422,8 @@ module GraphqlHelpers
end
end
- def query_double(schema:)
- double('query', schema: schema)
+ def query_double(schema: empty_schema)
+ double('query', schema: schema, warden: GraphQL::Schema::Warden::PassThruWarden)
end
def wrap_fields(fields)
@@ -380,7 +445,7 @@ module GraphqlHelpers
FIELDS
end
- def all_graphql_fields_for(class_name, parent_types = Set.new, max_depth: 3, excluded: [])
+ def all_graphql_fields_for(class_name, max_depth: 3, excluded: [])
# pulling _all_ fields can generate a _huge_ query (like complexity 180,000),
# and significantly increase spec runtime. so limit the depth by default
return if max_depth <= 0
@@ -397,7 +462,7 @@ module GraphqlHelpers
# We can't guess arguments, so skip fields that require them
skip = ->(name, field) { excluded.include?(name) || required_arguments?(field) }
- ::Graphql::FieldSelection.select_fields(type, skip, parent_types, max_depth)
+ ::Graphql::FieldSelection.select_fields(type, skip, max_depth)
end
def with_signature(variables, query)
@@ -569,8 +634,11 @@ module GraphqlHelpers
# Helps migrate to the new GraphQL interpreter,
# https://gitlab.com/gitlab-org/gitlab/-/issues/210556
- def expect_graphql_error_to_be_created(error_class, match_message = nil)
- expect { yield }.to raise_error(error_class, match_message)
+ def expect_graphql_error_to_be_created(error_class, match_message = '')
+ resolved = yield
+
+ expect(resolved).to be_instance_of(error_class)
+ expect(resolved.message).to match(match_message)
end
def flattened_errors
@@ -644,7 +712,7 @@ module GraphqlHelpers
end
def allow_high_graphql_recursion
- allow_any_instance_of(Gitlab::Graphql::QueryAnalyzers::RecursionAnalyzer).to receive(:recursion_threshold).and_return 1000
+ allow_any_instance_of(Gitlab::Graphql::QueryAnalyzers::AST::RecursionAnalyzer).to receive(:recursion_threshold).and_return 1000
end
def allow_high_graphql_transaction_threshold
@@ -699,13 +767,13 @@ module GraphqlHelpers
end
# assumes query_string and user to be let-bound in the current context
- def execute_query(query_type, schema: empty_schema, graphql: query_string, raise_on_error: false)
+ def execute_query(query_type = Types::QueryType, schema: empty_schema, graphql: query_string, raise_on_error: false, variables: {})
schema.query(query_type)
r = schema.execute(
graphql,
context: { current_user: user },
- variables: {}
+ variables: variables
)
if raise_on_error && r.to_h['errors'].present?
@@ -717,7 +785,6 @@ module GraphqlHelpers
def empty_schema
Class.new(GraphQL::Schema) do
- use GraphQL::Pagination::Connections
use Gitlab::Graphql::Pagination::Connections
use BatchLoader::GraphQL
@@ -817,7 +884,3 @@ module GraphqlHelpers
object_type.fields[name] || (raise ArgumentError, "Unknown field #{name} for #{described_class.graphql_name}")
end
end
-
-# This warms our schema, doing this as part of loading the helpers to avoid
-# duplicate loading error when Rails tries autoload the types.
-GitlabSchema.graphql_definition