graphql 1.9.6 → 1.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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