graphql 1.9.6 → 1.9.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +5 -5
  2. data/lib/generators/graphql/install_generator.rb +2 -1
  3. data/lib/generators/graphql/templates/base_field.erb +7 -0
  4. data/lib/graphql/analysis/ast/visitor.rb +5 -2
  5. data/lib/graphql/execution/multiplex.rb +5 -1
  6. data/lib/graphql/query.rb +6 -1
  7. data/lib/graphql/relay/array_connection.rb +1 -1
  8. data/lib/graphql/schema.rb +15 -1
  9. data/lib/graphql/schema/argument.rb +5 -1
  10. data/lib/graphql/schema/input_object.rb +21 -12
  11. data/lib/graphql/schema/introspection_system.rb +6 -1
  12. data/lib/graphql/schema/member/has_arguments.rb +4 -2
  13. data/lib/graphql/schema/resolver.rb +8 -3
  14. data/lib/graphql/schema/subscription.rb +22 -0
  15. data/lib/graphql/schema/timeout.rb +109 -0
  16. data/lib/graphql/types/relay/base_edge.rb +0 -3
  17. data/lib/graphql/upgrader/member.rb +148 -111
  18. data/lib/graphql/version.rb +1 -1
  19. data/readme.md +1 -1
  20. data/spec/fixtures/upgrader/mutation.original.rb +28 -0
  21. data/spec/fixtures/upgrader/mutation.transformed.rb +28 -0
  22. data/spec/graphql/analysis/ast_spec.rb +27 -0
  23. data/spec/graphql/execution/instrumentation_spec.rb +34 -6
  24. data/spec/graphql/execution/multiplex_spec.rb +11 -0
  25. data/spec/graphql/internal_representation/rewrite_spec.rb +6 -1
  26. data/spec/graphql/schema/input_object_spec.rb +56 -7
  27. data/spec/graphql/schema/introspection_system_spec.rb +24 -0
  28. data/spec/graphql/schema/subscription_spec.rb +65 -0
  29. data/spec/graphql/schema/timeout_spec.rb +206 -0
  30. data/spec/integration/mongoid/star_trek/schema.rb +1 -2
  31. data/spec/integration/rails/graphql/input_object_spec.rb +19 -0
  32. data/spec/integration/rails/graphql/relay/array_connection_spec.rb +47 -28
  33. data/spec/integration/rails/graphql/schema_spec.rb +18 -0
  34. data/spec/integration/tmp/app/graphql/types/date_type.rb +14 -0
  35. data/spec/integration/tmp/dummy/Gemfile +50 -0
  36. data/spec/integration/tmp/dummy/README.md +24 -0
  37. data/spec/integration/tmp/dummy/Rakefile +6 -0
  38. data/spec/integration/tmp/dummy/app/assets/config/manifest.js +3 -0
  39. data/spec/integration/tmp/dummy/app/assets/javascripts/application.js +16 -0
  40. data/spec/integration/tmp/dummy/app/assets/javascripts/cable.js +13 -0
  41. data/spec/integration/tmp/dummy/app/assets/stylesheets/application.css +15 -0
  42. data/spec/integration/tmp/dummy/app/channels/application_cable/channel.rb +5 -0
  43. data/spec/integration/tmp/dummy/app/channels/application_cable/connection.rb +5 -0
  44. data/spec/integration/tmp/dummy/app/controllers/application_controller.rb +4 -0
  45. data/spec/integration/tmp/dummy/app/controllers/graphql_controller.rb +44 -0
  46. data/spec/integration/tmp/dummy/app/helpers/application_helper.rb +3 -0
  47. data/spec/integration/tmp/dummy/app/jobs/application_job.rb +3 -0
  48. data/spec/integration/tmp/dummy/app/mailers/application_mailer.rb +5 -0
  49. data/spec/integration/tmp/dummy/app/mydirectory/dummy_schema.rb +5 -0
  50. data/spec/integration/tmp/dummy/app/mydirectory/mutations/update_name.rb +15 -0
  51. data/spec/integration/tmp/dummy/app/mydirectory/types/base_enum.rb +5 -0
  52. data/spec/integration/tmp/dummy/app/mydirectory/types/base_input_object.rb +5 -0
  53. data/spec/integration/tmp/dummy/app/mydirectory/types/base_interface.rb +6 -0
  54. data/spec/integration/tmp/dummy/app/mydirectory/types/base_object.rb +5 -0
  55. data/spec/integration/tmp/dummy/app/mydirectory/types/base_scalar.rb +5 -0
  56. data/spec/integration/tmp/dummy/app/mydirectory/types/base_union.rb +5 -0
  57. data/spec/integration/tmp/dummy/app/mydirectory/types/mutation_type.rb +12 -0
  58. data/spec/integration/tmp/dummy/app/mydirectory/types/query_type.rb +14 -0
  59. data/spec/integration/tmp/dummy/app/views/layouts/application.html.erb +14 -0
  60. data/spec/integration/tmp/dummy/app/views/layouts/mailer.html.erb +13 -0
  61. data/spec/integration/tmp/dummy/app/views/layouts/mailer.text.erb +1 -0
  62. data/spec/integration/tmp/dummy/bin/bundle +3 -0
  63. data/spec/integration/tmp/dummy/bin/rails +4 -0
  64. data/spec/integration/tmp/dummy/bin/rake +4 -0
  65. data/spec/integration/tmp/dummy/bin/setup +34 -0
  66. data/spec/integration/tmp/dummy/bin/update +29 -0
  67. data/spec/integration/tmp/dummy/config.ru +5 -0
  68. data/spec/integration/tmp/dummy/config/application.rb +26 -0
  69. data/spec/integration/tmp/dummy/config/boot.rb +4 -0
  70. data/spec/integration/tmp/dummy/config/cable.yml +9 -0
  71. data/spec/integration/tmp/dummy/config/environment.rb +6 -0
  72. data/spec/integration/tmp/dummy/config/environments/development.rb +52 -0
  73. data/spec/integration/tmp/dummy/config/environments/production.rb +84 -0
  74. data/spec/integration/tmp/dummy/config/environments/test.rb +43 -0
  75. data/spec/integration/tmp/dummy/config/initializers/application_controller_renderer.rb +9 -0
  76. data/spec/integration/tmp/dummy/config/initializers/assets.rb +12 -0
  77. data/spec/integration/tmp/dummy/config/initializers/backtrace_silencers.rb +8 -0
  78. data/spec/integration/tmp/dummy/config/initializers/cookies_serializer.rb +6 -0
  79. data/spec/integration/tmp/dummy/config/initializers/filter_parameter_logging.rb +5 -0
  80. data/spec/integration/tmp/dummy/config/initializers/inflections.rb +17 -0
  81. data/spec/integration/tmp/dummy/config/initializers/mime_types.rb +5 -0
  82. data/spec/integration/tmp/dummy/config/initializers/new_framework_defaults.rb +24 -0
  83. data/spec/integration/tmp/dummy/config/initializers/session_store.rb +4 -0
  84. data/spec/integration/tmp/dummy/config/initializers/wrap_parameters.rb +10 -0
  85. data/spec/integration/tmp/dummy/config/locales/en.yml +23 -0
  86. data/spec/integration/tmp/dummy/config/puma.rb +48 -0
  87. data/spec/integration/tmp/dummy/config/routes.rb +9 -0
  88. data/spec/integration/tmp/dummy/config/secrets.yml +22 -0
  89. data/spec/integration/tmp/dummy/db/seeds.rb +8 -0
  90. data/spec/integration/tmp/dummy/log/test.log +0 -0
  91. data/spec/integration/tmp/dummy/public/404.html +67 -0
  92. data/spec/integration/tmp/dummy/public/422.html +67 -0
  93. data/spec/integration/tmp/dummy/public/500.html +66 -0
  94. data/spec/integration/tmp/dummy/public/apple-touch-icon-precomposed.png +0 -0
  95. data/spec/integration/tmp/dummy/public/apple-touch-icon.png +0 -0
  96. data/spec/integration/tmp/dummy/public/favicon.ico +0 -0
  97. data/spec/integration/tmp/dummy/public/robots.txt +5 -0
  98. data/spec/integration/tmp/dummy/test/test_helper.rb +8 -0
  99. data/spec/support/jazz.rb +6 -0
  100. data/spec/support/star_wars/schema.rb +1 -2
  101. metadata +171 -6
  102. data/spec/integration/tmp/app/graphql/types/bird_type.rb +0 -7
@@ -62,23 +62,45 @@ describe GraphQL::Schema do
62
62
  end
63
63
  end
64
64
 
65
+ # This is how you might add queries from a persisted query backend
66
+
67
+ class QueryStringInstrumenter
68
+ def before_query(query)
69
+ if query.context[:extra_query_string] && query.query_string.nil?
70
+ query.query_string = query.context[:extra_query_string]
71
+ end
72
+ end
73
+
74
+ def after_query(query)
75
+ end
76
+ end
77
+
65
78
  let(:query_type) {
66
- GraphQL::ObjectType.define do
67
- name "Query"
68
- field :int, types.Int do
69
- argument :value, types.Int
70
- resolve ->(obj, args, ctx) { args.value }
79
+ Class.new(GraphQL::Schema::Object) do
80
+ graphql_name "Query"
81
+ field :int, Integer, null: true do
82
+ argument :value, Integer, required: false
83
+ end
84
+
85
+ def int(value:)
86
+ value
71
87
  end
72
88
  end
73
89
  }
74
90
 
75
91
  let(:schema) {
76
92
  spec = self
77
- GraphQL::Schema.define do
93
+ Class.new(GraphQL::Schema) do
78
94
  query(spec.query_type)
79
95
  instrument(:query, FirstInstrumenter.new)
80
96
  instrument(:query, SecondInstrumenter.new)
81
97
  instrument(:query, ExecutionErrorInstrumenter.new)
98
+ instrument(:query, QueryStringInstrumenter.new)
99
+
100
+ if TESTING_INTERPRETER
101
+ use GraphQL::Analysis::AST
102
+ use GraphQL::Execution::Interpreter
103
+ end
82
104
  end
83
105
  }
84
106
 
@@ -114,6 +136,12 @@ describe GraphQL::Schema do
114
136
  assert_equal "Raised from instrumenter before_query", res["errors"].first["message"]
115
137
  refute res.key?("data"), "The query doesn't run"
116
138
  end
139
+
140
+ it "can assign a query string there" do
141
+ context = { extra_query_string: "{ __typename }"}
142
+ res = schema.execute(nil, context: context)
143
+ assert_equal "Query", res["data"]["__typename"]
144
+ end
117
145
  end
118
146
 
119
147
  describe "within a multiplex" do
@@ -134,6 +134,17 @@ describe GraphQL::Execution::Multiplex do
134
134
  end
135
135
  end
136
136
 
137
+ describe "max_complexity" do
138
+ it "can successfully calculate complexity" do
139
+ message = "Query has complexity of 11, which exceeds max complexity of 10"
140
+ results = multiplex(queries, max_complexity: 10)
141
+
142
+ results.each do |res|
143
+ assert_equal message, res["errors"][0]["message"]
144
+ end
145
+ end
146
+ end
147
+
137
148
  describe "after_query when errors are raised" do
138
149
  class InspectQueryInstrumentation
139
150
  class << self
@@ -125,7 +125,12 @@ describe GraphQL::InternalRepresentation::Rewrite do
125
125
  nut_selections = plant_selection.typed_children[schema.types["Nut"]]
126
126
  # `... on Tree`, `... on Nut`, and `NutFields`, but not `... on Fruit { ... on Tree }`
127
127
 
128
- assert_equal 3, nut_selections["leafType"].ast_nodes.size
128
+ if RUBY_VERSION < "2.5"
129
+ # Ruby 2.5 changed how hash key collisions worked a little bit.
130
+ # This test is "broken", but honestly, it doesn't matter.
131
+ # Apparently nobody uses `IrepNode#ast_nodes`, and that's for the best.
132
+ assert_equal 3, nut_selections["leafType"].ast_nodes.size
133
+ end
129
134
 
130
135
  # Multi-level merging when including fragments:
131
136
  habitats_selections = nut_selections["habitats"].typed_children[schema.types["Habitat"]]
@@ -40,7 +40,7 @@ describe GraphQL::Schema::InputObject do
40
40
  ensemble_class = Class.new(subclass) do
41
41
  argument :ensemble_id, GraphQL::Types::ID, required: false, loads: Jazz::Ensemble
42
42
  end
43
-
43
+
44
44
  assert_equal 3, subclass.arguments.size
45
45
  assert_equal ["arg1", "arg2", "arg3"], subclass.arguments.keys
46
46
  assert_equal ["String!", "Int!", "Int!"], subclass.arguments.values.map { |a| a.type.to_type_signature }
@@ -87,8 +87,32 @@ describe GraphQL::Schema::InputObject do
87
87
  end
88
88
  end
89
89
 
90
+ class Mutation < GraphQL::Schema::Object
91
+ class TouchInstrument < GraphQL::Schema::Mutation
92
+ class InstrumentInput < GraphQL::Schema::InputObject
93
+ argument :instrument_id, ID, required: true, loads: Jazz::InstrumentType
94
+ end
95
+
96
+ argument :input_obj, InstrumentInput, required: true
97
+ field :instrument_name_method, String, null: false
98
+ field :instrument_name_key, String, null: false
99
+
100
+ def resolve(input_obj:)
101
+ # Make sure both kinds of access work the same:
102
+ {
103
+ instrument_name_method: input_obj.instrument.name,
104
+ instrument_name_key: input_obj[:instrument].name,
105
+ }
106
+ end
107
+ end
108
+
109
+ field :touch_instrument, mutation: TouchInstrument
110
+ end
111
+
112
+
90
113
  class Schema < GraphQL::Schema
91
114
  query(Query)
115
+ mutation(Mutation)
92
116
  if TESTING_INTERPRETER
93
117
  use GraphQL::Execution::Interpreter
94
118
  end
@@ -109,7 +133,7 @@ describe GraphQL::Schema::InputObject do
109
133
  GRAPHQL
110
134
 
111
135
  res = InputObjectPrepareTest::Schema.execute(query_str, context: { multiply_by: 3 })
112
- expected_obj = [{ a: 1, b2: 2, c: 9, d2: 12, e2: 30, instrument: "Instrument/Drum Kit" }.inspect, "Drum Kit"]
136
+ expected_obj = [{ a: 1, b2: 2, c: 9, d2: 12, e2: 30, instrument: Jazz::Models::Instrument.new("Drum Kit", "PERCUSSION") }.inspect, "Drum Kit"]
113
137
  assert_equal expected_obj, res["data"]["inputs"]
114
138
  end
115
139
 
@@ -127,6 +151,21 @@ describe GraphQL::Schema::InputObject do
127
151
  assert_equal("boom!", res["errors"][0]["extensions"]["problems"][0]["explanation"])
128
152
  assert_equal(input, res["errors"][0]["extensions"]["value"])
129
153
  end
154
+
155
+ it "loads input object arguments" do
156
+ query_str = <<-GRAPHQL
157
+ mutation {
158
+ touchInstrument(inputObj: { instrumentId: "Instrument/Drum Kit" }) {
159
+ instrumentNameMethod
160
+ instrumentNameKey
161
+ }
162
+ }
163
+ GRAPHQL
164
+
165
+ res = InputObjectPrepareTest::Schema.execute(query_str)
166
+ assert_equal "Drum Kit", res["data"]["touchInstrument"]["instrumentNameMethod"]
167
+ assert_equal "Drum Kit", res["data"]["touchInstrument"]["instrumentNameKey"]
168
+ end
130
169
  end
131
170
 
132
171
  describe "loading application object(s)" do
@@ -238,7 +277,7 @@ describe GraphQL::Schema::InputObject do
238
277
  end
239
278
  end
240
279
 
241
- describe "#to_h" do
280
+ describe 'hash conversion behavior' do
242
281
  module InputObjectToHTest
243
282
  class TestInput1 < GraphQL::Schema::InputObject
244
283
  graphql_name "TestInput1"
@@ -258,16 +297,26 @@ describe GraphQL::Schema::InputObject do
258
297
  TestInput2.to_graphql
259
298
  end
260
299
 
261
- it "returns a symbolized, aliased, ruby keyword style hash" do
300
+ before do
262
301
  arg_values = {a: 1, b: 2, c: { d: 3, e: 4, instrumentId: "Instrument/Drum Kit"}}
263
302
 
264
- input_object = InputObjectToHTest::TestInput2.new(
303
+ @input_object = InputObjectToHTest::TestInput2.new(
265
304
  arg_values,
266
- context: nil,
305
+ context: OpenStruct.new(schema: Jazz::Schema),
267
306
  defaults_used: Set.new
268
307
  )
308
+ end
269
309
 
270
- assert_equal({ a: 1, b: 2, input_object: { d: 3, e: 4, instrument: "Instrument/Drum Kit" } }, input_object.to_h)
310
+ describe "#to_h" do
311
+ it "returns a symbolized, aliased, ruby keyword style hash" do
312
+ assert_equal({ a: 1, b: 2, input_object: { d: 3, e: 4, instrument: Jazz::Models::Instrument.new("Drum Kit", "PERCUSSION") } }, @input_object.to_h)
313
+ end
314
+ end
315
+
316
+ describe "#to_hash" do
317
+ it "returns the same results as #to_h (aliased)" do
318
+ assert_equal(@input_object.to_h, @input_object.to_hash)
319
+ end
271
320
  end
272
321
  end
273
322
 
@@ -53,4 +53,28 @@ describe GraphQL::Schema::IntrospectionSystem do
53
53
  assert_equal [], ensembles_field["args"]
54
54
  end
55
55
  end
56
+
57
+ describe "#disable_introspection_entry_points" do
58
+ let(:schema) { Jazz::Schema }
59
+
60
+ it "allows entry point introspection by default" do
61
+ res = schema.execute("{ __schema { types { name } } }")
62
+ assert res
63
+
64
+ types = res["data"]["__schema"]["types"]
65
+ refute_empty types
66
+ end
67
+
68
+ describe "when entry points introspection is disabled" do
69
+ let(:schema) { Jazz::SchemaWithoutIntrospection }
70
+
71
+ it "returns error" do
72
+ res = schema.execute("{ __schema { types { name } } }")
73
+ assert res
74
+
75
+ assert_nil res["data"]
76
+ assert_equal ["Field '__schema' doesn't exist on type 'Query'"], res["errors"].map { |e| e["message"] }
77
+ end
78
+ end
79
+ end
56
80
  end
@@ -68,6 +68,12 @@ describe GraphQL::Schema::Subscription do
68
68
  end
69
69
  end
70
70
 
71
+ class DirectTootWasTooted < BaseSubscription
72
+ subscription_scope :viewer
73
+ field :toot, Toot, null: false
74
+ field :user, User, null: false
75
+ end
76
+
71
77
  # Test initial response, which returns all users
72
78
  class UsersJoined < BaseSubscription
73
79
  class UsersJoinedManualPayload < GraphQL::Schema::Object
@@ -97,6 +103,7 @@ describe GraphQL::Schema::Subscription do
97
103
  class Subscription < GraphQL::Schema::Object
98
104
  extend GraphQL::Subscriptions::SubscriptionRoot
99
105
  field :toot_was_tooted, subscription: TootWasTooted
106
+ field :direct_toot_was_tooted, subscription: DirectTootWasTooted
100
107
  field :users_joined, subscription: UsersJoined
101
108
  field :new_users_joined, subscription: NewUsersJoined
102
109
  end
@@ -437,4 +444,62 @@ describe GraphQL::Schema::Subscription do
437
444
  assert_equal 1, in_memory_subscription_count
438
445
  end
439
446
  end
447
+
448
+ describe "`subscription_scope` method" do
449
+ it "provdes a subscription scope that is recognized in the schema" do
450
+ scoped_subscription = SubscriptionFieldSchema::get_field("Subscription", "directTootWasTooted")
451
+
452
+ assert_equal :viewer, scoped_subscription.subscription_scope
453
+ end
454
+
455
+ it "provides a subscription scope that is used in execution" do
456
+ res = exec_query <<-GRAPHQL, context: { viewer: :me }
457
+ subscription {
458
+ directTootWasTooted {
459
+ toot { body }
460
+ }
461
+ }
462
+ GRAPHQL
463
+ assert_equal 1, in_memory_subscription_count
464
+
465
+ # Only the subscription with scope :me should be in the mailbox
466
+ obj = OpenStruct.new(toot: { body: "Hello from matz!" }, user: SubscriptionFieldSchema::USERS["matz"])
467
+ SubscriptionFieldSchema.subscriptions.trigger(:direct_toot_was_tooted, {}, obj, scope: :me)
468
+ SubscriptionFieldSchema.subscriptions.trigger(:direct_toot_was_tooted, {}, obj, scope: :not_me)
469
+ mailbox = res.context[:subscription_mailbox]
470
+
471
+ assert_equal 1, mailbox.length
472
+
473
+ expected_response = {
474
+ "data" => {
475
+ "directTootWasTooted" => {
476
+ "toot" => {
477
+ "body" => "Hello from matz!"
478
+ }
479
+ }
480
+ }
481
+ }
482
+
483
+ assert_equal expected_response, mailbox.first
484
+ end
485
+
486
+ it "allows for proper inheritance of the class's configuration in subclasses" do
487
+ # Make a subclass without an explicit configuration
488
+ class DirectTootSubclass < SubscriptionFieldSchema::DirectTootWasTooted
489
+ end
490
+ # Then check if the field options got the inherited value
491
+ direct_toot_options = DirectTootSubclass.field_options
492
+ assert_equal :viewer, direct_toot_options[:subscription_scope]
493
+ end
494
+
495
+ it "allows for setting the subscription scope value to nil" do
496
+ class PrivateSubscription < SubscriptionFieldSchema::BaseSubscription
497
+ subscription_scope :private
498
+ end
499
+
500
+ PrivateSubscription.subscription_scope nil
501
+
502
+ assert_nil PrivateSubscription.subscription_scope
503
+ end
504
+ end
440
505
  end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ describe GraphQL::Schema::Timeout do
5
+ let(:max_seconds) { 1 }
6
+ let(:timeout_class) { GraphQL::Schema::Timeout }
7
+ let(:timeout_schema) {
8
+ nested_sleep_type = Class.new(GraphQL::Schema::Object) do
9
+ graphql_name "NestedSleep"
10
+
11
+ field :seconds, Float, null: true
12
+
13
+ def seconds
14
+ object
15
+ end
16
+
17
+ field :nested_sleep, GraphQL::Schema::LateBoundType.new(graphql_name), null: true do
18
+ argument :seconds, Float, required: true
19
+ end
20
+
21
+ def nested_sleep(seconds:)
22
+ sleep(seconds)
23
+ seconds
24
+ end
25
+ end
26
+
27
+ query_type = Class.new(GraphQL::Schema::Object) do
28
+ graphql_name "Query"
29
+
30
+ field :sleep_for, Float, null: true do
31
+ argument :seconds, Float, required: true
32
+ end
33
+
34
+ def sleep_for(seconds:)
35
+ sleep(seconds)
36
+ seconds
37
+ end
38
+
39
+ field :nested_sleep, nested_sleep_type, null: true do
40
+ argument :seconds, Float, required: true
41
+ end
42
+
43
+ def nested_sleep(seconds:)
44
+ sleep(seconds)
45
+ seconds
46
+ end
47
+ end
48
+
49
+ schema = Class.new(GraphQL::Schema) do
50
+ query query_type
51
+ if TESTING_INTERPRETER
52
+ use GraphQL::Execution::Interpreter
53
+ end
54
+ end
55
+ schema.use timeout_class, max_seconds: max_seconds
56
+ schema
57
+ }
58
+
59
+ let(:result) { timeout_schema.execute(query_string) }
60
+
61
+ describe "timeout part-way through" do
62
+ let(:query_string) {%|
63
+ {
64
+ a: sleepFor(seconds: 0.4)
65
+ b: sleepFor(seconds: 0.4)
66
+ c: sleepFor(seconds: 0.4)
67
+ d: sleepFor(seconds: 0.4)
68
+ e: sleepFor(seconds: 0.4)
69
+ }
70
+ |}
71
+ it "returns a partial response and error messages" do
72
+ expected_data = {
73
+ "a"=>0.4,
74
+ "b"=>0.4,
75
+ "c"=>0.4,
76
+ "d"=>nil,
77
+ "e"=>nil,
78
+ }
79
+
80
+ expected_errors = [
81
+ {
82
+ "message"=>"Timeout on Query.sleepFor",
83
+ "locations"=>[{"line"=>6, "column"=>9}],
84
+ "path"=>["d"]
85
+ },
86
+ {
87
+ "message"=>"Timeout on Query.sleepFor",
88
+ "locations"=>[{"line"=>7, "column"=>9}],
89
+ "path"=>["e"]
90
+ },
91
+ ]
92
+ assert_equal expected_data, result["data"]
93
+ assert_equal expected_errors, result["errors"]
94
+ end
95
+ end
96
+
97
+ describe "timeout in nested fields" do
98
+ let(:query_string) {%|
99
+ {
100
+ a: nestedSleep(seconds: 0.3) {
101
+ seconds
102
+ b: nestedSleep(seconds: 0.3) {
103
+ seconds
104
+ c: nestedSleep(seconds: 0.3) {
105
+ seconds
106
+ d: nestedSleep(seconds: 0.4) {
107
+ seconds
108
+ e: nestedSleep(seconds: 0.4) {
109
+ seconds
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ |}
117
+
118
+ it "returns a partial response and error messages" do
119
+ expected_data = {
120
+ "a" => {
121
+ "seconds" => 0.3,
122
+ "b" => {
123
+ "seconds" => 0.3,
124
+ "c" => {
125
+ "seconds"=>0.3,
126
+ "d" => {
127
+ "seconds"=>nil,
128
+ "e"=>nil
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ expected_errors = [
135
+ {
136
+ "message"=>"Timeout on NestedSleep.seconds",
137
+ "locations"=>[{"line"=>10, "column"=>15}],
138
+ "path"=>["a", "b", "c", "d", "seconds"]
139
+ },
140
+ {
141
+ "message"=>"Timeout on NestedSleep.nestedSleep",
142
+ "locations"=>[{"line"=>11, "column"=>15}],
143
+ "path"=>["a", "b", "c", "d", "e"]
144
+ },
145
+ ]
146
+
147
+ assert_equal expected_data, result["data"]
148
+ assert_equal expected_errors, result["errors"]
149
+ end
150
+ end
151
+
152
+ describe "long-running fields" do
153
+ let(:query_string) {%|
154
+ {
155
+ a: sleepFor(seconds: 0.2)
156
+ b: sleepFor(seconds: 0.2)
157
+ c: sleepFor(seconds: 0.8)
158
+ d: sleepFor(seconds: 0.1)
159
+ }
160
+ |}
161
+ it "doesn't terminate long-running field execution" do
162
+ expected_data = {
163
+ "a"=>0.2,
164
+ "b"=>0.2,
165
+ "c"=>0.8,
166
+ "d"=>nil,
167
+ }
168
+
169
+ expected_errors = [
170
+ {
171
+ "message"=>"Timeout on Query.sleepFor",
172
+ "locations"=>[{"line"=>6, "column"=>9}],
173
+ "path"=>["d"]
174
+ },
175
+ ]
176
+
177
+ assert_equal expected_data, result["data"]
178
+ assert_equal expected_errors, result["errors"]
179
+ end
180
+ end
181
+
182
+ describe "with a custom block" do
183
+ let(:timeout_class) do
184
+ Class.new(GraphQL::Schema::Timeout) do
185
+ def handle_timeout(err, query)
186
+ raise("Query timed out after 2s: #{err.message}")
187
+ end
188
+ end
189
+ end
190
+
191
+ let(:query_string) {%|
192
+ {
193
+ a: sleepFor(seconds: 0.4)
194
+ b: sleepFor(seconds: 0.4)
195
+ c: sleepFor(seconds: 0.4)
196
+ d: sleepFor(seconds: 0.4)
197
+ e: sleepFor(seconds: 0.4)
198
+ }
199
+ |}
200
+
201
+ it "calls the block" do
202
+ err = assert_raises(RuntimeError) { result }
203
+ assert_equal "Query timed out after 2s: Timeout on Query.sleepFor", err.message
204
+ end
205
+ end
206
+ end