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.rb206
1 files changed, 127 insertions, 79 deletions
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index a1b4e6eee92..b20801bd3c4 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -4,6 +4,11 @@ module GraphqlHelpers
MutationDefinition = Struct.new(:query, :variables)
NoData = Class.new(StandardError)
+ UnauthorizedObject = Class.new(StandardError)
+
+ def graphql_args(**values)
+ ::Graphql::Arguments.new(values)
+ end
# makes an underscored string look like a fieldname
# "merge_request" => "mergeRequest"
@@ -17,7 +22,10 @@ module GraphqlHelpers
# ready, then the early return is returned instead.
#
# Then the resolve method is called.
- def resolve(resolver_class, args: {}, **resolver_args)
+ def resolve(resolver_class, args: {}, lookahead: :not_given, parent: :not_given, **resolver_args)
+ args = aliased_args(resolver_class, args)
+ args[:parent] = parent unless parent == :not_given
+ args[:lookahead] = lookahead unless lookahead == :not_given
resolver = resolver_instance(resolver_class, **resolver_args)
ready, early_return = sync_all { resolver.ready?(**args) }
@@ -26,6 +34,16 @@ module GraphqlHelpers
resolver.resolve(**args)
end
+ # TODO: Remove this method entirely when GraphqlHelpers uses real resolve_field
+ # see: https://gitlab.com/gitlab-org/gitlab/-/issues/287791
+ def aliased_args(resolver, args)
+ definitions = resolver.arguments
+
+ args.transform_keys do |k|
+ definitions[GraphqlHelpers.fieldnamerize(k)]&.keyword || k
+ end
+ end
+
def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema)
if ctx.is_a?(Hash)
q = double('Query', schema: schema)
@@ -111,24 +129,33 @@ module GraphqlHelpers
def variables_for_mutation(name, input)
graphql_input = prepare_input_for_mutation(input)
- result = { input_variable_name_for_mutation(name) => graphql_input }
+ { input_variable_name_for_mutation(name) => graphql_input }
+ end
- # Avoid trying to serialize multipart data into JSON
- if graphql_input.values.none? { |value| io_value?(value) }
- result.to_json
- else
- result
- end
+ def serialize_variables(variables)
+ return unless variables
+ return variables if variables.is_a?(String)
+
+ ::Gitlab::Utils::MergeHash.merge(Array.wrap(variables).map(&:to_h)).to_json
end
- def resolve_field(name, object, args = {})
- context = double("Context",
- schema: GitlabSchema,
- query: GraphQL::Query.new(GitlabSchema),
- parent: nil)
- field = described_class.fields[name]
+ def resolve_field(name, object, args = {}, current_user: nil)
+ q = GraphQL::Query.new(GitlabSchema)
+ context = GraphQL::Query::Context.new(query: q, object: object, values: { current_user: current_user })
+ allow(context).to receive(:parent).and_return(nil)
+ field = described_class.fields.fetch(GraphqlHelpers.fieldnamerize(name))
instance = described_class.authorized_new(object, context)
- field.resolve_field(instance, {}, context)
+ raise UnauthorizedObject unless instance
+
+ field.resolve_field(instance, args, context)
+ end
+
+ def simple_resolver(resolved_value = 'Resolved value')
+ Class.new(Resolvers::BaseResolver) do
+ define_method :resolve do |**_args|
+ resolved_value
+ end
+ end
end
# Recursively convert a Hash with Ruby-style keys to GraphQL fieldname-style keys
@@ -165,10 +192,32 @@ module GraphqlHelpers
end
def query_graphql_field(name, attributes = {}, fields = nil)
- <<~QUERY
- #{field_with_params(name, attributes)}
- #{wrap_fields(fields || all_graphql_fields_for(name.to_s.classify))}
- QUERY
+ attributes, fields = [nil, attributes] if fields.nil? && !attributes.is_a?(Hash)
+
+ field = field_with_params(name, attributes)
+
+ field + wrap_fields(fields || all_graphql_fields_for(name.to_s.classify)).to_s
+ end
+
+ def page_info_selection
+ "pageInfo { hasNextPage hasPreviousPage endCursor startCursor }"
+ end
+
+ def query_nodes(name, fields = nil, args: nil, of: name, include_pagination_info: false, max_depth: 1)
+ fields ||= all_graphql_fields_for(of.to_s.classify, max_depth: max_depth)
+ node_selection = include_pagination_info ? "#{page_info_selection} nodes" : :nodes
+ query_graphql_path([[name, args], node_selection], fields)
+ end
+
+ # e.g:
+ # query_graphql_path(%i[foo bar baz], all_graphql_fields_for('Baz'))
+ # => foo { bar { baz { x y z } } }
+ def query_graphql_path(segments, fields = nil)
+ # we really want foldr here...
+ segments.reverse.reduce(fields) do |tail, segment|
+ name, args = Array.wrap(segment)
+ query_graphql_field(name, args, tail)
+ end
end
def wrap_fields(fields)
@@ -203,50 +252,22 @@ module GraphqlHelpers
type = GitlabSchema.types[class_name.to_s]
return "" unless type
- type.fields.map do |name, field|
- # We can't guess arguments, so skip fields that require them
- next if required_arguments?(field)
- next if excluded.include?(name)
-
- singular_field_type = field_type(field)
-
- # If field type is the same as parent type, then we're hitting into
- # mutual dependency. Break it from infinite recursion
- next if parent_types.include?(singular_field_type)
+ # We can't guess arguments, so skip fields that require them
+ skip = ->(name, field) { excluded.include?(name) || required_arguments?(field) }
- if nested_fields?(field)
- fields =
- all_graphql_fields_for(singular_field_type, parent_types | [type], max_depth: max_depth - 1)
-
- "#{name} { #{fields} }" unless fields.blank?
- else
- name
- end
- end.compact.join("\n")
+ ::Graphql::FieldSelection.select_fields(type, skip, parent_types, max_depth)
end
- def attributes_to_graphql(attributes)
- attributes.map do |name, value|
- value_str = as_graphql_literal(value)
+ def with_signature(variables, query)
+ %Q[query(#{variables.map(&:sig).join(', ')}) #{query}]
+ end
- "#{GraphqlHelpers.fieldnamerize(name.to_s)}: #{value_str}"
- end.join(", ")
+ def var(type)
+ ::Graphql::Var.new(generate(:variable), type)
end
- # Fairly dumb Ruby => GraphQL rendering function. Only suitable for testing.
- # Use symbol for Enum values
- def as_graphql_literal(value)
- case value
- when Array then "[#{value.map { |v| as_graphql_literal(v) }.join(',')}]"
- when Hash then "{#{attributes_to_graphql(value)}}"
- when Integer, Float then value.to_s
- when String then "\"#{value.gsub(/"/, '\\"')}\""
- when Symbol then value
- when nil then 'null'
- when true then 'true'
- when false then 'false'
- else raise ArgumentError, "Cannot represent #{value} as GraphQL literal"
- end
+ def attributes_to_graphql(arguments)
+ ::Graphql::Arguments.new(arguments).to_s
end
def post_multiplex(queries, current_user: nil, headers: {})
@@ -254,7 +275,7 @@ module GraphqlHelpers
end
def post_graphql(query, current_user: nil, variables: nil, headers: {})
- params = { query: query, variables: variables&.to_json }
+ params = { query: query, variables: serialize_variables(variables) }
post api('/', current_user, version: 'graphql'), params: params, headers: headers
end
@@ -320,36 +341,47 @@ module GraphqlHelpers
{ operations: operations.to_json, map: map.to_json }.merge(extracted_files)
end
+ def fresh_response_data
+ Gitlab::Json.parse(response.body)
+ end
+
# Raises an error if no data is found
- def graphql_data
+ def graphql_data(body = json_response)
# Note that `json_response` is defined as `let(:json_response)` and
# therefore, in a spec with multiple queries, will only contain data
# from the _first_ query, not subsequent ones
- json_response['data'] || (raise NoData, graphql_errors)
+ body['data'] || (raise NoData, graphql_errors(body))
end
def graphql_data_at(*path)
graphql_dig_at(graphql_data, *path)
end
+ # Slightly more powerful than just `dig`:
+ # - also supports implicit flat-mapping (.e.g. :foo :nodes :bar :nodes)
def graphql_dig_at(data, *path)
keys = path.map { |segment| segment.is_a?(Integer) ? segment : GraphqlHelpers.fieldnamerize(segment) }
# Allows for array indexing, like this
# ['project', 'boards', 'edges', 0, 'node', 'lists']
keys.reduce(data) do |memo, key|
- memo.is_a?(Array) ? memo[key] : memo&.dig(key)
+ if memo.is_a?(Array)
+ key.is_a?(Integer) ? memo[key] : memo.flat_map { |e| Array.wrap(e[key]) }
+ else
+ memo&.dig(key)
+ end
end
end
- def graphql_errors
- case json_response
+ # See note at graphql_data about memoization and multiple requests
+ def graphql_errors(body = json_response)
+ case body
when Hash # regular query
- json_response['errors']
+ body['errors']
when Array # multiplexed queries
- json_response.map { |response| response['errors'] }
+ body.map { |response| response['errors'] }
else
- raise "Unknown GraphQL response type #{json_response.class}"
+ raise "Unknown GraphQL response type #{body.class}"
end
end
@@ -392,19 +424,29 @@ module GraphqlHelpers
end
def nested_fields?(field)
- !scalar?(field) && !enum?(field)
+ ::Graphql::FieldInspection.new(field).nested_fields?
end
def scalar?(field)
- field_type(field).kind.scalar?
+ ::Graphql::FieldInspection.new(field).scalar?
end
def enum?(field)
- field_type(field).kind.enum?
+ ::Graphql::FieldInspection.new(field).enum?
end
+ # There are a few non BaseField fields in our schema (pageInfo for one).
+ # None of them require arguments.
def required_arguments?(field)
- field.arguments.values.any? { |argument| argument.type.non_null? }
+ return field.requires_argument? if field.is_a?(::Types::BaseField)
+
+ if (meta = field.try(:metadata)) && meta[:type_class]
+ required_arguments?(meta[:type_class])
+ elsif args = field.try(:arguments)
+ args.values.any? { |argument| argument.type.non_null? }
+ else
+ false
+ end
end
def io_value?(value)
@@ -412,15 +454,7 @@ module GraphqlHelpers
end
def field_type(field)
- field_type = field.type.respond_to?(:to_graphql) ? field.type.to_graphql : field.type
-
- # The type could be nested. For example `[GraphQL::STRING_TYPE]`:
- # - List
- # - String!
- # - String
- field_type = field_type.of_type while field_type.respond_to?(:of_type)
-
- field_type
+ ::Graphql::FieldInspection.new(field).type
end
# for most tests, we want to allow unlimited complexity
@@ -498,6 +532,20 @@ module GraphqlHelpers
variables: {}
)
end
+
+ # A lookahead that selects everything
+ def positive_lookahead
+ double(selects?: true).tap do |selection|
+ allow(selection).to receive(:selection).and_return(selection)
+ end
+ end
+
+ # A lookahead that selects nothing
+ def negative_lookahead
+ double(selects?: false).tap do |selection|
+ allow(selection).to receive(:selection).and_return(selection)
+ end
+ end
end
# This warms our schema, doing this as part of loading the helpers to avoid