Repository URL to install this package:
|
Version:
1.8.2 ▾
|
# frozen_string_literal: true
require "spec_helper"
describe GraphQL::Query do
let(:query_string) { %|
query getFlavor($cheeseId: Int!) {
brie: cheese(id: 1) { ...cheeseFields, taste: flavor },
cheese(id: $cheeseId) {
__typename,
id,
...cheeseFields,
... edibleFields,
... on Cheese { cheeseKind: flavor },
}
fromSource(source: COW) { id }
fromSheep: fromSource(source: SHEEP) { id }
firstSheep: searchDairy(product: [{source: SHEEP}]) {
__typename,
... dairyFields,
... milkFields
}
favoriteEdible { __typename, fatContent }
}
fragment cheeseFields on Cheese { flavor }
fragment edibleFields on Edible { fatContent }
fragment milkFields on Milk { source }
fragment dairyFields on AnimalProduct {
... on Cheese { flavor }
... on Milk { source }
}
|}
let(:operation_name) { nil }
let(:max_depth) { nil }
let(:query_variables) { {"cheeseId" => 2} }
let(:schema) { Dummy::Schema }
let(:document) { GraphQL.parse(query_string) }
let(:query) { GraphQL::Query.new(
schema,
query_string,
variables: query_variables,
operation_name: operation_name,
max_depth: max_depth,
)}
let(:result) { query.result }
describe "when passed both a query string and a document" do
it "returns an error to the client when query kwarg is used" do
assert_raises ArgumentError do
GraphQL::Query.new(
schema,
query: "{ fromSource(source: COW) { id } }",
document: document
)
end
end
it "returns an error to the client" do
assert_raises ArgumentError do
GraphQL::Query.new(
schema,
"{ fromSource(source: COW) { id } }",
document: document
)
end
end
end
describe "when passed no query string or document" do
it 'returns an error to the client' do
res = GraphQL::Query.new(
schema,
variables: query_variables,
operation_name: operation_name,
max_depth: max_depth,
).result
assert_equal 1, res["errors"].length
assert_equal "No query string was present", res["errors"][0]["message"]
end
it 'can be assigned later' do
query = GraphQL::Query.new(
schema,
variables: query_variables,
operation_name: operation_name,
max_depth: max_depth,
)
query.query_string = '{ __type(name: """Cheese""") { name } }'
assert_equal "Cheese", query.result["data"] ["__type"]["name"]
end
end
describe "#operation_name" do
describe "when provided" do
let(:query_string) { <<-GRAPHQL
query q1 { cheese(id: 1) { flavor } }
query q2 { cheese(id: 2) { flavor } }
GRAPHQL
}
let(:operation_name) { "q2" }
it "returns the provided name" do
assert_equal "q2", query.operation_name
end
end
describe "when inferred" do
let(:query_string) { <<-GRAPHQL
query q3 { cheese(id: 3) { flavor } }
GRAPHQL
}
it "returns nil" do
assert_nil query.operation_name
end
end
describe "#selected_operation_name" do
describe "when an operation isprovided" do
let(:query_string) { <<-GRAPHQL
query q1 { cheese(id: 1) { flavor } }
query q2 { cheese(id: 2) { flavor } }
GRAPHQL
}
let(:operation_name) { "q2" }
it "returns the provided name" do
assert_equal "q2", query.selected_operation_name
end
end
describe "when operation is inferred" do
let(:query_string) { <<-GRAPHQL
query q3 { cheese(id: 3) { flavor } }
GRAPHQL
}
it "returns the inferred operation name" do
assert_equal "q3", query.selected_operation_name
end
end
describe "when there are no operations" do
let(:query_string) { <<-GRAPHQL
# Only Comments
# In this Query
GRAPHQL
}
it "returns the inferred operation name" do
assert_nil query.selected_operation_name
end
end
end
describe "assigning operation_name=" do
let(:query_string) { <<-GRAPHQL
query q3 { manchego: cheese(id: 3) { flavor } }
query q2 { gouda: cheese(id: 2) { flavor } }
GRAPHQL
}
it "runs the assigned name" do
query = GraphQL::Query.new(Dummy::Schema, query_string, operation_name: "q3")
query.operation_name = "q2"
res = query.result
assert_equal "Gouda", res["data"]["gouda"]["flavor"]
end
end
end
describe "when passed a document instance" do
let(:query) { GraphQL::Query.new(
schema,
document: document,
variables: query_variables,
operation_name: operation_name,
max_depth: max_depth,
)}
it "runs the query using the already parsed document" do
expected = {"data"=> {
"brie" => { "flavor" => "Brie", "taste" => "Brie" },
"cheese" => {
"__typename" => "Cheese",
"id" => 2,
"flavor" => "Gouda",
"fatContent" => 0.3,
"cheeseKind" => "Gouda",
},
"fromSource" => [{ "id" => 1 }, {"id" => 2}],
"fromSheep"=>[{"id"=>3}],
"firstSheep" => { "__typename" => "Cheese", "flavor" => "Manchego" },
"favoriteEdible"=>{"__typename"=>"Milk", "fatContent"=>0.04},
}}
assert_equal(expected, result)
end
end
describe '#result' do
it "returns fields on objects" do
expected = {"data"=> {
"brie" => { "flavor" => "Brie", "taste" => "Brie" },
"cheese" => {
"__typename" => "Cheese",
"id" => 2,
"flavor" => "Gouda",
"fatContent" => 0.3,
"cheeseKind" => "Gouda",
},
"fromSource" => [{ "id" => 1 }, {"id" => 2}],
"fromSheep"=>[{"id"=>3}],
"firstSheep" => { "__typename" => "Cheese", "flavor" => "Manchego" },
"favoriteEdible"=>{"__typename"=>"Milk", "fatContent"=>0.04},
}}
assert_equal(expected, result)
end
describe "when it hits null objects" do
let(:query_string) {%|
{
maybeNull {
cheese {
flavor,
similarCheese(source: [SHEEP]) { flavor }
}
}
}
|}
it "skips null objects" do
expected = {"data"=> {
"maybeNull" => { "cheese" => nil }
}}
assert_equal(expected, result)
end
end
describe "after_query hooks" do
module Instrumenter
ERROR_LOG = []
def self.before_query(q); end;
def self.after_query(q); ERROR_LOG << q.result["errors"]; end;
end
let(:schema) {
Dummy::Schema.redefine {
instrument(:query, Instrumenter)
}
}
it "can access #result" do
result
assert_equal [nil], Instrumenter::ERROR_LOG
end
end
describe "when an error propagated through execution" do
module ExtensionsInstrumenter
LOG = []
def self.before_query(q); end;
def self.after_query(q)
q.result["extensions"] = { "a" => 1 }
LOG << :ok
end
end
let(:schema) {
Dummy::Schema.redefine {
instrument(:query, ExtensionsInstrumenter)
}
}
it "can add to extensions" do
ExtensionsInstrumenter::LOG.clear
assert_raises(RuntimeError) do
schema.execute "{ error }"
end
assert_equal [:ok], ExtensionsInstrumenter::LOG
end
end
end
it "uses root_value as the object for the root type" do
result = GraphQL::Query.new(schema, '{ root }', root_value: "I am root").result
assert_equal 'I am root', result.fetch('data').fetch('root')
end
it "exposes fragments" do
assert_equal(GraphQL::Language::Nodes::FragmentDefinition, query.fragments["cheeseFields"].class)
end
it "exposes the original string" do
assert_equal(query_string, query.query_string)
end
describe "merging fragments with different keys" do
let(:query_string) { %|
query getCheeseFieldsThroughDairy {
... cheeseFrag3
dairy {
...flavorFragment
...fatContentFragment
}
}
fragment flavorFragment on Dairy {
cheese {
flavor
}
milks {
id
}
}
fragment fatContentFragment on Dairy {
cheese {
fatContent
}
milks {
fatContent
}
}
fragment cheeseFrag1 on Query {
cheese(id: 1) {
id
}
}
fragment cheeseFrag2 on Query {
cheese(id: 1) {
flavor
}
}
fragment cheeseFrag3 on Query {
... cheeseFrag2
... cheeseFrag1
}
|}
it "should include keys from each fragment" do
expected = {"data" => {
"dairy" => {
"cheese" => {
"flavor" => "Brie",
"fatContent" => 0.19
},
"milks" => [
{
"id" => "1",
"fatContent" => 0.04,
}
],
},
"cheese" => {
"id" => 1,
"flavor" => "Brie"
},
}}
assert_equal(expected, result)
end
end
describe "field argument default values" do
let(:query_string) {%|
query getCheeses(
$search: [DairyProductInput]
$searchWithDefault: [DairyProductInput] = [{source: COW}]
){
noVariable: searchDairy(product: $search) {
... cheeseFields
}
noArgument: searchDairy {
... cheeseFields
}
variableDefault: searchDairy(product: $searchWithDefault) {
... cheeseFields
}
convertedDefault: fromSource {
... cheeseFields
}
}
fragment cheeseFields on Cheese { flavor }
|}
it "has a default value" do
default_source = schema.query.fields["searchDairy"].arguments["product"].default_value[0]["source"]
assert_equal("SHEEP", default_source)
end
describe "when a variable is used, but not provided" do
it "uses the default_value" do
assert_equal("Manchego", result["data"]["noVariable"]["flavor"])
end
end
describe "when the argument isn't passed at all" do
it "uses the default value" do
assert_equal("Manchego", result["data"]["noArgument"]["flavor"])
end
end
describe "when the variable has a default" do
it "uses the variable default" do
assert_equal("Brie", result["data"]["variableDefault"]["flavor"])
end
end
describe "when the variable has a default needing conversion" do
it "uses the converted variable default" do
assert_equal([{"flavor" => "Brie"}, {"flavor" => "Gouda"}], result["data"]["convertedDefault"])
end
end
end
describe "query variables" do
let(:query_string) {%|
query getCheese($cheeseId: Int!){
cheese(id: $cheeseId) { flavor }
}
|}
describe "when they can be coerced" do
let(:query_variables) { {"cheeseId" => 2.0} }
it "coerces them on the way in" do
assert("Gouda", result["data"]["cheese"]["flavor"])
end
end
describe "when they can't be coerced" do
let(:query_variables) { {"cheeseId" => "2"} }
it "raises an error" do
expected = {
"errors" => [
{
"message" => "Variable cheeseId of type Int! was provided invalid value",
"locations"=>[{ "line" => 2, "column" => 23 }],
"value" => "2",
"problems" => [{ "path" => [], "explanation" => 'Could not coerce value "2" to Int' }]
}
]
}
assert_equal(expected, result)
end
end
describe "when they aren't provided" do
let(:query_variables) { {} }
it "raises an error" do
expected = {
"errors" => [
{
"message" => "Variable cheeseId of type Int! was provided invalid value",
"locations" => [{"line" => 2, "column" => 23}],
"value" => nil,
"problems" => [{ "path" => [], "explanation" => "Expected value to not be null" }]
}
]
}
assert_equal(expected, result)
end
end
describe "when they are non-null and provided a null value" do
let(:query_variables) { { "cheeseId" => nil } }
it "raises an error" do
expected = {
"errors" => [
{
"message" => "Variable cheeseId of type Int! was provided invalid value",
"locations" => [{"line" => 2, "column" => 23}],
"value" => nil,
"problems" => [{ "path" => [], "explanation" => "Expected value to not be null" }]
}
]
}
assert_equal(expected, result)
end
end
describe "when they're a string" do
let(:query_variables) { '{ "var" : 1 }' }
it "raises an error" do
assert_raises(ArgumentError) { result }
end
end
describe "default values" do
let(:query_string) {%|
query getCheese($cheeseId: Int = 3){
cheese(id: $cheeseId) { id, flavor }
}
|}
describe "when no value is provided" do
let(:query_variables) { {} }
it "uses the default" do
assert(3, result["data"]["cheese"]["id"])
assert("Manchego", result["data"]["cheese"]["flavor"])
end
end
describe "when a value is provided" do
it "uses the provided variable" do
assert(2, result["data"]["cheese"]["id"])
assert("Gouda", result["data"]["cheese"]["flavor"])
end
end
describe "when complex values" do
let(:query_variables) { {"search" => [{"source" => "COW"}]} }
let(:query_string) {%|
query getCheeses($search: [DairyProductInput]!){
cow: searchDairy(product: $search) {
... on Cheese {
flavor
}
}
}
|}
it "coerces recursively" do
assert_equal("Brie", result["data"]["cow"]["flavor"])
end
end
end
end
describe "max_depth" do
let(:query_string) {
<<-GRAPHQL
{
cheese(id: 1) {
similarCheese(source: SHEEP) {
similarCheese(source: SHEEP) {
similarCheese(source: SHEEP) {
similarCheese(source: SHEEP) {
id
}
}
}
}
}
}
GRAPHQL
}
it "defaults to the schema's max_depth" do
# Constrained by schema's setting of 5
assert_equal 1, result["errors"].length
end
describe "overriding max_depth" do
let(:max_depth) { 12 }
it "overrides the schema's max_depth" do
assert result["data"].key?("cheese")
assert_nil result["errors"]
end
end
end
describe "#provided_variables" do
it "returns the originally-provided object" do
assert_equal({"cheeseId" => 2}, query.provided_variables)
end
end
describe "parse errors" do
let(:invalid_query_string) {
<<-GRAPHQL
{
getStuff
nonsense
This is broken 1
}
GRAPHQL
}
it "adds an entry to the errors key" do
res = schema.execute(" { ")
assert_equal 1, res["errors"].length
assert_equal "Unexpected end of document", res["errors"][0]["message"]
assert_equal [], res["errors"][0]["locations"]
res = schema.execute(invalid_query_string)
assert_equal 1, res["errors"].length
assert_equal %|Parse error on "1" (INT) at [4, 26]|, res["errors"][0]["message"]
assert_equal({"line" => 4, "column" => 26}, res["errors"][0]["locations"][0])
end
it "can be configured to raise" do
raise_schema = schema.redefine(parse_error: ->(err, ctx) { raise err })
assert_raises(GraphQL::ParseError) {
raise_schema.execute(invalid_query_string)
}
end
end
describe "#mutation?" do
let(:query_string) { <<-GRAPHQL
query Q { __typename }
mutation M { pushValue(value: 1) }
GRAPHQL
}
it "returns true if the selected operation is a mutation" do
query_query = GraphQL::Query.new(schema, query_string, operation_name: "Q")
assert_equal false, query_query.mutation?
assert_equal true, query_query.query?
mutation_query = GraphQL::Query.new(schema, query_string, operation_name: "M")
assert_equal true, mutation_query.mutation?
assert_equal false, mutation_query.query?
end
end
describe "validate: false" do
it "doesn't validate the query" do
invalid_query_string = "{ nonExistantField }"
# Can assign attribute
query = GraphQL::Query.new(schema, invalid_query_string)
query.validate = false
assert_equal true, query.valid?
assert_equal 0, query.static_errors.length
# Can pass keyword argument
query = GraphQL::Query.new(schema, invalid_query_string, validate: false)
assert_equal true, query.valid?
assert_equal 0, query.static_errors.length
# Can pass `true`
query = GraphQL::Query.new(schema, invalid_query_string, validate: true)
assert_equal false, query.valid?
assert_equal 1, query.static_errors.length
end
end
describe 'NullValue type arguments' do
let(:schema_definition) {
<<-GRAPHQL
type Query {
foo(id: [ID]): Int
}
GRAPHQL
}
let(:expected_args) { [] }
let(:default_resolver) do
{
'Query' => { 'foo' => ->(_obj, args, _ctx) { expected_args.push(args); 1 } },
}
end
let(:schema) { GraphQL::Schema.from_definition(schema_definition, default_resolve: default_resolver) }
it 'sets argument to nil when null is passed' do
query = <<-GRAPHQL
{
foo(id: null)
}
GRAPHQL
schema.execute(query)
assert(expected_args.first.key?('id'))
assert_nil(expected_args.first['id'])
end
it 'sets argument to nil when nil is passed via variable' do
query = <<-GRAPHQL
query baz($id: [ID]) {
foo(id: $id)
}
GRAPHQL
schema.execute(query, variables: { 'id' => nil })
assert(expected_args.first.key?('id'))
assert([nil], expected_args.first['id'])
end
it 'sets argument to [nil] when [null] is passed' do
query = <<-GRAPHQL
{
foo(id: [null])
}
GRAPHQL
schema.execute(query)
assert(expected_args.first.key?('id'))
assert_equal([nil], expected_args.first['id'])
end
it 'sets argument to [nil] when [nil] is passed via variable' do
query = <<-GRAPHQL
query baz($id: [ID]) {
foo(id: $id)
}
GRAPHQL
schema.execute(query, variables: { 'id' => [nil] })
assert(expected_args.first.key?('id'))
assert_equal([nil], expected_args.first['id'])
end
end
describe '#internal_representation' do
it "includes all definition roots" do
assert_kind_of GraphQL::InternalRepresentation::Node, query.internal_representation.operation_definitions["getFlavor"]
assert_kind_of GraphQL::InternalRepresentation::Node, query.internal_representation.fragment_definitions["cheeseFields"]
assert_kind_of GraphQL::InternalRepresentation::Node, query.internal_representation.fragment_definitions["edibleFields"]
assert_kind_of GraphQL::InternalRepresentation::Node, query.internal_representation.fragment_definitions["milkFields"]
assert_kind_of GraphQL::InternalRepresentation::Node, query.internal_representation.fragment_definitions["dairyFields"]
end
end
describe '#irep_selection' do
it "returns the irep for the selected operation" do
assert_kind_of GraphQL::InternalRepresentation::Node, query.irep_selection
assert_equal 'getFlavor', query.irep_selection.name
end
it "returns nil when there is no selected operation" do
query = GraphQL::Query.new(schema, '# Only a comment')
assert_nil query.irep_selection
end
end
describe "query_execution_strategy" do
let(:custom_execution_schema) {
schema.redefine do
query_execution_strategy DummyStrategy
instrument(:multiplex, DummyMultiplexInstrumenter)
end
}
class DummyStrategy
def execute(ast_operation, root_type, query_object)
{ "dummy" => true }
end
end
class DummyMultiplexInstrumenter
def self.before_multiplex(m)
m.queries.first.context[:before_multiplex] = true
end
def self.after_multiplex(m)
end
end
it "is used for running a query, if it's present and not the default" do
result = custom_execution_schema.execute(" { __typename }")
assert_equal({"data"=>{"dummy"=>true}}, result)
result = custom_execution_schema.execute(" mutation { __typename } ")
assert_equal({"data"=>{"__typename" => "Mutation"}}, result)
end
it "treats the query as a one-item multiplex" do
ctx = {}
custom_execution_schema.execute(" { __typename }", context: ctx)
assert_equal true, ctx[:before_multiplex]
end
it "can't run a multiplex" do
err = assert_raises ArgumentError do
custom_execution_schema.multiplex([
{query: " { __typename }"},
{query: " { __typename }"},
])
end
msg = "Multiplexing doesn't support custom execution strategies, run one query at a time instead"
assert_equal msg, err.message
end
end
end