graphql 1.8.3 → 1.8.4

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +4 -1
  3. data/lib/graphql/argument.rb +1 -0
  4. data/lib/graphql/authorization.rb +81 -0
  5. data/lib/graphql/boolean_type.rb +0 -1
  6. data/lib/graphql/compatibility/lazy_execution_specification/lazy_schema.rb +2 -1
  7. data/lib/graphql/execution/execute.rb +34 -10
  8. data/lib/graphql/execution/lazy.rb +5 -1
  9. data/lib/graphql/field.rb +7 -1
  10. data/lib/graphql/float_type.rb +0 -1
  11. data/lib/graphql/id_type.rb +0 -1
  12. data/lib/graphql/int_type.rb +0 -1
  13. data/lib/graphql/introspection/entry_points.rb +2 -2
  14. data/lib/graphql/object_type.rb +3 -3
  15. data/lib/graphql/query.rb +6 -0
  16. data/lib/graphql/query/arguments.rb +2 -0
  17. data/lib/graphql/query/context.rb +6 -0
  18. data/lib/graphql/query/variables.rb +7 -1
  19. data/lib/graphql/relay/connection_instrumentation.rb +2 -2
  20. data/lib/graphql/relay/connection_resolve.rb +7 -27
  21. data/lib/graphql/relay/connection_type.rb +1 -0
  22. data/lib/graphql/relay/edge_type.rb +1 -0
  23. data/lib/graphql/relay/edges_instrumentation.rb +9 -25
  24. data/lib/graphql/relay/mutation/instrumentation.rb +1 -2
  25. data/lib/graphql/relay/mutation/resolve.rb +2 -4
  26. data/lib/graphql/relay/node.rb +1 -6
  27. data/lib/graphql/relay/page_info.rb +1 -9
  28. data/lib/graphql/schema.rb +84 -11
  29. data/lib/graphql/schema/argument.rb +13 -0
  30. data/lib/graphql/schema/enum.rb +1 -1
  31. data/lib/graphql/schema/enum_value.rb +4 -0
  32. data/lib/graphql/schema/field.rb +44 -11
  33. data/lib/graphql/schema/interface.rb +20 -0
  34. data/lib/graphql/schema/introspection_system.rb +1 -1
  35. data/lib/graphql/schema/member/base_dsl_methods.rb +25 -3
  36. data/lib/graphql/schema/member/instrumentation.rb +15 -17
  37. data/lib/graphql/schema/mutation.rb +4 -0
  38. data/lib/graphql/schema/object.rb +33 -0
  39. data/lib/graphql/schema/possible_types.rb +2 -0
  40. data/lib/graphql/schema/resolver.rb +10 -0
  41. data/lib/graphql/schema/traversal.rb +9 -2
  42. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +11 -2
  43. data/lib/graphql/string_type.rb +0 -1
  44. data/lib/graphql/types.rb +7 -0
  45. data/lib/graphql/types/relay.rb +31 -0
  46. data/lib/graphql/types/relay/base_connection.rb +87 -0
  47. data/lib/graphql/types/relay/base_edge.rb +51 -0
  48. data/lib/graphql/types/relay/base_field.rb +22 -0
  49. data/lib/graphql/types/relay/base_interface.rb +29 -0
  50. data/lib/graphql/types/relay/base_object.rb +26 -0
  51. data/lib/graphql/types/relay/node.rb +18 -0
  52. data/lib/graphql/types/relay/page_info.rb +23 -0
  53. data/lib/graphql/unauthorized_error.rb +20 -0
  54. data/lib/graphql/version.rb +1 -1
  55. data/spec/graphql/authorization_spec.rb +684 -0
  56. data/spec/graphql/query/variables_spec.rb +20 -0
  57. data/spec/graphql/relay/connection_instrumentation_spec.rb +1 -1
  58. data/spec/graphql/schema/resolver_spec.rb +31 -0
  59. data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +52 -0
  60. data/spec/support/dummy/schema.rb +16 -0
  61. data/spec/support/star_wars/schema.rb +28 -17
  62. metadata +15 -2
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class UnauthorizedError < GraphQL::Error
4
+ # @return [Object] the application object that failed the authorization check
5
+ attr_reader :object
6
+
7
+ # @return [Class] the GraphQL object type whose `.authorized?` method was called (and returned false)
8
+ attr_reader :type
9
+
10
+ # @return [GraphQL::Query::Context] the context for the current query
11
+ attr_reader :context
12
+
13
+ def initialize(object:, type:, context:)
14
+ @object = object
15
+ @type = type
16
+ @context = context
17
+ super("An instance of #{object.class} failed #{type.name}'s authorization check")
18
+ end
19
+ end
20
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.8.3"
3
+ VERSION = "1.8.4"
4
4
  end
@@ -0,0 +1,684 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ describe GraphQL::Authorization do
5
+ module AuthTest
6
+ class Box
7
+ attr_reader :value
8
+ def initialize(value:)
9
+ @value = value
10
+ end
11
+ end
12
+
13
+ class BaseArgument < GraphQL::Schema::Argument
14
+ def visible?(context)
15
+ super && (context[:hide] ? @name != "hidden" : true)
16
+ end
17
+
18
+ def accessible?(context)
19
+ super && (context[:hide] ? @name != "inaccessible" : true)
20
+ end
21
+
22
+ def authorized?(parent_object, context)
23
+ super && parent_object != :hide2
24
+ end
25
+ end
26
+
27
+ class BaseField < GraphQL::Schema::Field
28
+ def initialize(*args, edge_class: nil, **kwargs, &block)
29
+ @edge_class = edge_class
30
+ super(*args, **kwargs, &block)
31
+ end
32
+
33
+ def to_graphql
34
+ field_defn = super
35
+ if @edge_class
36
+ field_defn.edge_class = @edge_class
37
+ end
38
+ field_defn
39
+ end
40
+
41
+ argument_class BaseArgument
42
+ def visible?(context)
43
+ super && (context[:hide] ? @name != "hidden" : true)
44
+ end
45
+
46
+ def accessible?(context)
47
+ super && (context[:hide] ? @name != "inaccessible" : true)
48
+ end
49
+
50
+ def authorized?(object, context)
51
+ super && object != :hide
52
+ end
53
+ end
54
+
55
+ class BaseObject < GraphQL::Schema::Object
56
+ field_class BaseField
57
+ end
58
+
59
+ module BaseInterface
60
+ include GraphQL::Schema::Interface
61
+ end
62
+
63
+ class BaseEnumValue < GraphQL::Schema::EnumValue
64
+ def initialize(*args, role: nil, **kwargs)
65
+ @role = role
66
+ super(*args, **kwargs)
67
+ end
68
+
69
+ def visible?(context)
70
+ super && (context[:hide] ? @role != :hidden : true)
71
+ end
72
+ end
73
+
74
+ class BaseEnum < GraphQL::Schema::Enum
75
+ enum_value_class(BaseEnumValue)
76
+ end
77
+
78
+ module HiddenInterface
79
+ include BaseInterface
80
+
81
+ def self.visible?(ctx)
82
+ super && !ctx[:hide]
83
+ end
84
+
85
+ def self.resolve_type(obj, ctx)
86
+ HiddenObject
87
+ end
88
+ end
89
+
90
+ module HiddenDefaultInterface
91
+ include BaseInterface
92
+ # visible? will call the super method
93
+ def self.resolve_type(obj, ctx)
94
+ HiddenObject
95
+ end
96
+ end
97
+
98
+ class HiddenObject < BaseObject
99
+ implements HiddenInterface
100
+ implements HiddenDefaultInterface
101
+ def self.visible?(ctx)
102
+ super && !ctx[:hide]
103
+ end
104
+ end
105
+
106
+ class RelayObject < BaseObject
107
+ def self.visible?(ctx)
108
+ super && !ctx[:hidden_relay]
109
+ end
110
+
111
+ def self.accessible?(ctx)
112
+ super && !ctx[:inaccessible_relay]
113
+ end
114
+
115
+ def self.authorized?(_val, ctx)
116
+ super && !ctx[:unauthorized_relay]
117
+ end
118
+ end
119
+
120
+ # TODO test default behavior for abstract types,
121
+ # that they check their concrete types
122
+ module InaccessibleInterface
123
+ include BaseInterface
124
+
125
+ def self.accessible?(ctx)
126
+ super && !ctx[:hide]
127
+ end
128
+
129
+ def self.resolve_type(obj, ctx)
130
+ InaccessibleObject
131
+ end
132
+ end
133
+
134
+ module InaccessibleDefaultInterface
135
+ include BaseInterface
136
+ # accessible? will call the super method
137
+ def self.resolve_type(obj, ctx)
138
+ InaccessibleObject
139
+ end
140
+ end
141
+
142
+ class InaccessibleObject < BaseObject
143
+ implements InaccessibleInterface
144
+ implements InaccessibleDefaultInterface
145
+ def self.accessible?(ctx)
146
+ super && !ctx[:hide]
147
+ end
148
+ end
149
+
150
+ class UnauthorizedObject < BaseObject
151
+ def self.authorized?(value, context)
152
+ super && !context[:hide]
153
+ end
154
+ end
155
+
156
+ class UnauthorizedBox < BaseObject
157
+ # Hide `"a"`
158
+ def self.authorized?(value, context)
159
+ super && value != "a"
160
+ end
161
+
162
+ field :value, String, null: false, method: :object
163
+ end
164
+
165
+ module UnauthorizedInterface
166
+ include BaseInterface
167
+
168
+ def self.resolve_type(obj, ctx)
169
+ if obj.is_a?(String)
170
+ UnauthorizedCheckBox
171
+ else
172
+ raise "Unexpected value: #{obj.inspect}"
173
+ end
174
+ end
175
+ end
176
+
177
+ class UnauthorizedCheckBox < BaseObject
178
+ implements UnauthorizedInterface
179
+ # This authorized check returns a lazy object, it should be synced by the runtime.
180
+ def self.authorized?(value, context)
181
+ if !value.is_a?(String)
182
+ raise "Unexpected box value: #{value.inspect}"
183
+ end
184
+ is_authed = super && value != "a"
185
+ # Make it many levels nested just to make sure we support nested lazy objects
186
+ Box.new(value: Box.new(value: Box.new(value: Box.new(value: is_authed))))
187
+ end
188
+
189
+ field :value, String, null: false, method: :object
190
+ end
191
+
192
+ class IntegerObject < BaseObject
193
+ def self.authorized?(obj, ctx)
194
+ if !obj.is_a?(Integer)
195
+ raise "Unexpected IntegerObject: #{obj}"
196
+ end
197
+ is_allowed = !(ctx[:unauthorized_relay] || obj == ctx[:exclude_integer])
198
+ Box.new(value: Box.new(value: is_allowed))
199
+ end
200
+ field :value, Integer, null: false, method: :object
201
+ end
202
+
203
+ class IntegerObjectEdge < GraphQL::Types::Relay::BaseEdge
204
+ node_type(IntegerObject)
205
+ end
206
+
207
+ class IntegerObjectConnection < GraphQL::Types::Relay::BaseConnection
208
+ edge_type(IntegerObjectEdge)
209
+ end
210
+
211
+ class LandscapeFeature < BaseEnum
212
+ value "MOUNTAIN"
213
+ value "STREAM", role: :unauthorized
214
+ value "FIELD", role: :inaccessible
215
+ value "TAR_PIT", role: :hidden
216
+ end
217
+
218
+ class Query < BaseObject
219
+ field :hidden, Integer, null: false
220
+ field :unauthorized, Integer, null: true, method: :object
221
+ field :int2, Integer, null: true do
222
+ argument :int, Integer, required: false
223
+ argument :hidden, Integer, required: false
224
+ argument :inaccessible, Integer, required: false
225
+ argument :unauthorized, Integer, required: false
226
+ end
227
+
228
+ def int2(**args)
229
+ args[:unauthorized] || 1
230
+ end
231
+
232
+ field :landscape_feature, LandscapeFeature, null: false do
233
+ argument :string, String, required: false
234
+ argument :enum, LandscapeFeature, required: false
235
+ end
236
+
237
+ def landscape_feature(string: nil, enum: nil)
238
+ string || enum
239
+ end
240
+
241
+ field :landscape_features, [LandscapeFeature], null: false do
242
+ argument :strings, [String], required: false
243
+ argument :enums, [LandscapeFeature], required: false
244
+ end
245
+
246
+ def landscape_features(strings: [], enums: [])
247
+ strings + enums
248
+ end
249
+
250
+ def empty_array; []; end
251
+ field :hidden_object, HiddenObject, null: false, method: :itself
252
+ field :hidden_interface, HiddenInterface, null: false, method: :itself
253
+ field :hidden_default_interface, HiddenDefaultInterface, null: false, method: :itself
254
+ field :hidden_connection, RelayObject.connection_type, null: :false, method: :empty_array
255
+ field :hidden_edge, RelayObject.edge_type, null: :false, method: :itself
256
+
257
+ field :inaccessible, Integer, null: false, method: :object_id
258
+ field :inaccessible_object, InaccessibleObject, null: false, method: :itself
259
+ field :inaccessible_interface, InaccessibleInterface, null: false, method: :itself
260
+ field :inaccessible_default_interface, InaccessibleDefaultInterface, null: false, method: :itself
261
+ field :inaccessible_connection, RelayObject.connection_type, null: :false, method: :empty_array
262
+ field :inaccessible_edge, RelayObject.edge_type, null: :false, method: :itself
263
+
264
+ field :unauthorized_object, UnauthorizedObject, null: true, method: :itself
265
+ field :unauthorized_connection, RelayObject.connection_type, null: :false, method: :empty_array
266
+ field :unauthorized_edge, RelayObject.edge_type, null: :false, method: :itself
267
+ field :unauthorized_lazy_box, UnauthorizedBox, null: true do
268
+ argument :value, String, required: true
269
+ end
270
+ def unauthorized_lazy_box(value:)
271
+ # Make it extra nested, just for good measure.
272
+ Box.new(value: Box.new(value: value))
273
+ end
274
+ field :unauthorized_list_items, [UnauthorizedObject], null: true
275
+ def unauthorized_list_items
276
+ [self, self]
277
+ end
278
+
279
+ field :unauthorized_lazy_check_box, UnauthorizedCheckBox, null: true, method: :unauthorized_lazy_box do
280
+ argument :value, String, required: true
281
+ end
282
+
283
+ field :unauthorized_interface, UnauthorizedInterface, null: true, method: :unauthorized_lazy_box do
284
+ argument :value, String, required: true
285
+ end
286
+
287
+ field :integers, IntegerObjectConnection, null: false
288
+
289
+ def integers
290
+ [1,2,3]
291
+ end
292
+
293
+ field :lazy_integers, IntegerObjectConnection, null: false
294
+
295
+ def lazy_integers
296
+ Box.new(value: Box.new(value: [1,2,3]))
297
+ end
298
+ end
299
+
300
+ class DoHiddenStuff < GraphQL::Schema::RelayClassicMutation
301
+ def self.visible?(ctx)
302
+ super && (ctx[:hidden_mutation] ? false : true)
303
+ end
304
+ end
305
+
306
+ class DoInaccessibleStuff < GraphQL::Schema::RelayClassicMutation
307
+ def self.accessible?(ctx)
308
+ super && (ctx[:inaccessible_mutation] ? false : true)
309
+ end
310
+ end
311
+
312
+ class DoUnauthorizedStuff < GraphQL::Schema::RelayClassicMutation
313
+ def self.authorized?(obj, ctx)
314
+ super && (ctx[:unauthorized_mutation] ? false : true)
315
+ end
316
+ end
317
+
318
+ class Mutation < BaseObject
319
+ field :do_hidden_stuff, mutation: DoHiddenStuff
320
+ field :do_inaccessible_stuff, mutation: DoInaccessibleStuff
321
+ field :do_unauthorized_stuff, mutation: DoUnauthorizedStuff
322
+ end
323
+
324
+ class Schema < GraphQL::Schema
325
+ query(Query)
326
+ mutation(Mutation)
327
+
328
+ lazy_resolve(Box, :value)
329
+
330
+ def self.unauthorized_object(err)
331
+ raise GraphQL::ExecutionError, "Unauthorized #{err.type.graphql_name}: #{err.object}"
332
+ end
333
+
334
+ # use GraphQL::Backtrace
335
+ end
336
+ end
337
+
338
+ def auth_execute(*args)
339
+ AuthTest::Schema.execute(*args)
340
+ end
341
+
342
+ describe "applying the visible? method" do
343
+ it "works in queries" do
344
+ res = auth_execute(" { int int2 } ", context: { hide: true })
345
+ assert_equal 1, res["errors"].size
346
+ end
347
+
348
+ it "applies return type visibility to fields" do
349
+ error_queries = {
350
+ "hiddenObject" => "{ hiddenObject { __typename } }",
351
+ "hiddenInterface" => "{ hiddenInterface { __typename } }",
352
+ "hiddenDefaultInterface" => "{ hiddenDefaultInterface { __typename } }",
353
+ }
354
+
355
+ error_queries.each do |name, q|
356
+ hidden_res = auth_execute(q, context: { hide: true})
357
+ assert_equal ["Field '#{name}' doesn't exist on type 'Query'"], hidden_res["errors"].map { |e| e["message"] }
358
+
359
+ visible_res = auth_execute(q)
360
+ # Both fields exist; the interface resolves to the object type, though
361
+ assert_equal "HiddenObject", visible_res["data"][name]["__typename"]
362
+ end
363
+ end
364
+
365
+ it "uses the mutation for derived fields, inputs and outputs" do
366
+ query = "mutation { doHiddenStuff(input: {}) { __typename } }"
367
+ res = auth_execute(query, context: { hidden_mutation: true })
368
+ assert_equal ["Field 'doHiddenStuff' doesn't exist on type 'Mutation'"], res["errors"].map { |e| e["message"] }
369
+
370
+ # `#resolve` isn't implemented, so this errors out:
371
+ assert_raises NotImplementedError do
372
+ auth_execute(query)
373
+ end
374
+
375
+ introspection_q = <<-GRAPHQL
376
+ {
377
+ t1: __type(name: "DoHiddenStuffInput") { name }
378
+ t2: __type(name: "DoHiddenStuffPayload") { name }
379
+ }
380
+ GRAPHQL
381
+ hidden_introspection_res = auth_execute(introspection_q, context: { hidden_mutation: true })
382
+ assert_nil hidden_introspection_res["data"]["t1"]
383
+ assert_nil hidden_introspection_res["data"]["t2"]
384
+
385
+ visible_introspection_res = auth_execute(introspection_q)
386
+ assert_equal "DoHiddenStuffInput", visible_introspection_res["data"]["t1"]["name"]
387
+ assert_equal "DoHiddenStuffPayload", visible_introspection_res["data"]["t2"]["name"]
388
+ end
389
+
390
+ it "uses the base type for edges and connections" do
391
+ query = <<-GRAPHQL
392
+ {
393
+ hiddenConnection { __typename }
394
+ hiddenEdge { __typename }
395
+ }
396
+ GRAPHQL
397
+
398
+ hidden_res = auth_execute(query, context: { hidden_relay: true })
399
+ assert_equal 2, hidden_res["errors"].size
400
+
401
+ visible_res = auth_execute(query)
402
+ assert_equal "RelayObjectConnection", visible_res["data"]["hiddenConnection"]["__typename"]
403
+ assert_equal "RelayObjectEdge", visible_res["data"]["hiddenEdge"]["__typename"]
404
+ end
405
+
406
+ it "treats hidden enum values as non-existant, even in lists" do
407
+ hidden_res_1 = auth_execute <<-GRAPHQL, context: { hide: true }
408
+ {
409
+ landscapeFeature(enum: TAR_PIT)
410
+ }
411
+ GRAPHQL
412
+
413
+ assert_equal ["Argument 'enum' on Field 'landscapeFeature' has an invalid value. Expected type 'LandscapeFeature'."], hidden_res_1["errors"].map { |e| e["message"] }
414
+
415
+ hidden_res_2 = auth_execute <<-GRAPHQL, context: { hide: true }
416
+ {
417
+ landscapeFeatures(enums: [STREAM, TAR_PIT])
418
+ }
419
+ GRAPHQL
420
+
421
+ assert_equal ["Argument 'enums' on Field 'landscapeFeatures' has an invalid value. Expected type '[LandscapeFeature!]'."], hidden_res_2["errors"].map { |e| e["message"] }
422
+
423
+ success_res = auth_execute <<-GRAPHQL, context: { hide: false }
424
+ {
425
+ landscapeFeature(enum: TAR_PIT)
426
+ landscapeFeatures(enums: [STREAM, TAR_PIT])
427
+ }
428
+ GRAPHQL
429
+
430
+ assert_equal "TAR_PIT", success_res["data"]["landscapeFeature"]
431
+ assert_equal ["STREAM", "TAR_PIT"], success_res["data"]["landscapeFeatures"]
432
+ end
433
+
434
+ it "refuses to resolve to hidden enum values" do
435
+ assert_raises(GraphQL::EnumType::UnresolvedValueError) do
436
+ auth_execute <<-GRAPHQL, context: { hide: true }
437
+ {
438
+ landscapeFeature(string: "TAR_PIT")
439
+ }
440
+ GRAPHQL
441
+ end
442
+
443
+ assert_raises(GraphQL::EnumType::UnresolvedValueError) do
444
+ auth_execute <<-GRAPHQL, context: { hide: true }
445
+ {
446
+ landscapeFeatures(strings: ["STREAM", "TAR_PIT"])
447
+ }
448
+ GRAPHQL
449
+ end
450
+ end
451
+
452
+ it "works in introspection" do
453
+ res = auth_execute <<-GRAPHQL, context: { hide: true, hidden_mutation: true }
454
+ {
455
+ query: __type(name: "Query") {
456
+ fields {
457
+ name
458
+ args { name }
459
+ }
460
+ }
461
+
462
+ hiddenObject: __type(name: "HiddenObject") { name }
463
+ hiddenInterface: __type(name: "HiddenInterface") { name }
464
+ landscapeFeatures: __type(name: "LandscapeFeature") { enumValues { name } }
465
+ }
466
+ GRAPHQL
467
+ query_field_names = res["data"]["query"]["fields"].map { |f| f["name"] }
468
+ refute_includes query_field_names, "int"
469
+ int2_arg_names = res["data"]["query"]["fields"].find { |f| f["name"] == "int2" }["args"].map { |a| a["name"] }
470
+ assert_equal ["int", "inaccessible", "unauthorized"], int2_arg_names
471
+
472
+ assert_nil res["data"]["hiddenObject"]
473
+ assert_nil res["data"]["hiddenInterface"]
474
+
475
+ visible_landscape_features = res["data"]["landscapeFeatures"]["enumValues"].map { |v| v["name"] }
476
+ assert_equal ["MOUNTAIN", "STREAM", "FIELD"], visible_landscape_features
477
+ end
478
+ end
479
+
480
+ describe "applying the accessible? method" do
481
+ it "works with fields and arguments" do
482
+ queries = {
483
+ "{ inaccessible }" => ["Some fields in this query are not accessible: inaccessible"],
484
+ "{ int2(inaccessible: 1) }" => ["Some fields in this query are not accessible: int2"],
485
+ }
486
+
487
+ queries.each do |query_str, errors|
488
+ res = auth_execute(query_str, context: { hide: true })
489
+ assert_equal errors, res.fetch("errors").map { |e| e["message"] }
490
+
491
+ res = auth_execute(query_str, context: { hide: false })
492
+ refute res.key?("errors")
493
+ end
494
+ end
495
+
496
+ it "works with return types" do
497
+ queries = {
498
+ "{ inaccessibleObject { __typename } }" => ["Some fields in this query are not accessible: inaccessibleObject"],
499
+ "{ inaccessibleInterface { __typename } }" => ["Some fields in this query are not accessible: inaccessibleInterface"],
500
+ "{ inaccessibleDefaultInterface { __typename } }" => ["Some fields in this query are not accessible: inaccessibleDefaultInterface"],
501
+ }
502
+
503
+ queries.each do |query_str, errors|
504
+ res = auth_execute(query_str, context: { hide: true })
505
+ assert_equal errors, res["errors"].map { |e| e["message"] }
506
+
507
+ res = auth_execute(query_str, context: { hide: false })
508
+ refute res.key?("errors")
509
+ end
510
+ end
511
+
512
+ it "works with mutations" do
513
+ query = "mutation { doInaccessibleStuff(input: {}) { __typename } }"
514
+ res = auth_execute(query, context: { inaccessible_mutation: true })
515
+ assert_equal ["Some fields in this query are not accessible: doInaccessibleStuff"], res["errors"].map { |e| e["message"] }
516
+
517
+ assert_raises NotImplementedError do
518
+ auth_execute(query)
519
+ end
520
+ end
521
+
522
+ it "works with edges and connections" do
523
+ query = <<-GRAPHQL
524
+ {
525
+ inaccessibleConnection { __typename }
526
+ inaccessibleEdge { __typename }
527
+ }
528
+ GRAPHQL
529
+
530
+ inaccessible_res = auth_execute(query, context: { inaccessible_relay: true })
531
+ assert_equal ["Some fields in this query are not accessible: inaccessibleConnection, inaccessibleEdge"], inaccessible_res["errors"].map { |e| e["message"] }
532
+
533
+ accessible_res = auth_execute(query)
534
+ refute accessible_res.key?("errors")
535
+ end
536
+ end
537
+
538
+ describe "applying the authorized? method" do
539
+ it "halts on unauthorized objects" do
540
+ query = "{ unauthorizedObject { __typename } }"
541
+ hidden_response = auth_execute(query, context: { hide: true })
542
+ assert_nil hidden_response["data"].fetch("unauthorizedObject")
543
+ visible_response = auth_execute(query, context: {})
544
+ assert_equal({ "__typename" => "UnauthorizedObject" }, visible_response["data"]["unauthorizedObject"])
545
+ end
546
+
547
+ it "halts on unauthorized mutations" do
548
+ query = "mutation { doUnauthorizedStuff(input: {}) { __typename } }"
549
+ res = auth_execute(query, context: { unauthorized_mutation: true })
550
+ assert_nil res["data"].fetch("doUnauthorizedStuff")
551
+ assert_raises NotImplementedError do
552
+ auth_execute(query)
553
+ end
554
+ end
555
+
556
+ it "halts on unauthorized fields, using the parent object" do
557
+ query = "{ unauthorized }"
558
+ hidden_response = auth_execute(query, root_value: :hide)
559
+ assert_nil hidden_response["data"].fetch("unauthorized")
560
+ visible_response = auth_execute(query, root_value: 1)
561
+ assert_equal 1, visible_response["data"]["unauthorized"]
562
+ end
563
+
564
+ it "halts on unauthorized arguments, using the parent object" do
565
+ query = "{ int2(unauthorized: 5) }"
566
+ hidden_response = auth_execute(query, root_value: :hide2)
567
+ assert_nil hidden_response["data"].fetch("int2")
568
+ visible_response = auth_execute(query)
569
+ assert_equal 5, visible_response["data"]["int2"]
570
+ end
571
+
572
+ it "works with edges and connections" do
573
+ skip <<-MSG
574
+ This doesn't work because edge and connection type definitions
575
+ aren't class-based, and authorization is checked during class-based field execution.
576
+ MSG
577
+ query = <<-GRAPHQL
578
+ {
579
+ unauthorizedConnection { __typename }
580
+ unauthorizedEdge { __typename }
581
+ }
582
+ GRAPHQL
583
+
584
+ unauthorized_res = auth_execute(query, context: { unauthorized_relay: true })
585
+ assert_nil unauthorized_res["data"].fetch("unauthorizedConnection")
586
+ assert_nil unauthorized_res["data"].fetch("unauthorizedEdge")
587
+
588
+ authorized_res = auth_execute(query)
589
+ assert_nil authorized_res["data"].fetch("unauthorizedConnection").fetch("__typename")
590
+ assert_nil authorized_res["data"].fetch("unauthorizedEdge").fetch("__typename")
591
+ end
592
+
593
+ it "authorizes _after_ resolving lazy objects" do
594
+ query = <<-GRAPHQL
595
+ {
596
+ a: unauthorizedLazyBox(value: "a") { value }
597
+ b: unauthorizedLazyBox(value: "b") { value }
598
+ }
599
+ GRAPHQL
600
+
601
+ unauthorized_res = auth_execute(query)
602
+ assert_nil unauthorized_res["data"].fetch("a")
603
+ assert_equal "b", unauthorized_res["data"]["b"]["value"]
604
+ end
605
+
606
+ it "authorizes items in a list" do
607
+ query = <<-GRAPHQL
608
+ {
609
+ unauthorizedListItems { __typename }
610
+ }
611
+ GRAPHQL
612
+
613
+ unauthorized_res = auth_execute(query, context: { hide: true })
614
+
615
+ assert_nil unauthorized_res["data"]["unauthorizedListItems"]
616
+ authorized_res = auth_execute(query, context: { hide: false })
617
+ assert_equal 2, authorized_res["data"]["unauthorizedListItems"].size
618
+ end
619
+
620
+ it "syncs lazy objects from authorized? checks" do
621
+ query = <<-GRAPHQL
622
+ {
623
+ a: unauthorizedLazyCheckBox(value: "a") { value }
624
+ b: unauthorizedLazyCheckBox(value: "b") { value }
625
+ }
626
+ GRAPHQL
627
+
628
+ unauthorized_res = auth_execute(query)
629
+ assert_nil unauthorized_res["data"].fetch("a")
630
+ assert_equal "b", unauthorized_res["data"]["b"]["value"]
631
+ # Also, the custom handler was called:
632
+ assert_equal ["Unauthorized UnauthorizedCheckBox: a"], unauthorized_res["errors"].map { |e| e["message"] }
633
+ end
634
+
635
+ it "Works for lazy connections" do
636
+ query = <<-GRAPHQL
637
+ {
638
+ lazyIntegers { edges { node { value } } }
639
+ }
640
+ GRAPHQL
641
+ res = auth_execute(query)
642
+ assert_equal [1,2,3], res["data"]["lazyIntegers"]["edges"].map { |e| e["node"]["value"] }
643
+ end
644
+
645
+ it "Works for eager connections" do
646
+ query = <<-GRAPHQL
647
+ {
648
+ integers { edges { node { value } } }
649
+ }
650
+ GRAPHQL
651
+ res = auth_execute(query)
652
+ assert_equal [1,2,3], res["data"]["integers"]["edges"].map { |e| e["node"]["value"] }
653
+ end
654
+
655
+ it "filters out individual nodes by value" do
656
+ query = <<-GRAPHQL
657
+ {
658
+ integers { edges { node { value } } }
659
+ }
660
+ GRAPHQL
661
+ res = auth_execute(query, context: { exclude_integer: 1 })
662
+ assert_equal [nil,2,3], res["data"]["integers"]["edges"].map { |e| e["node"] && e["node"]["value"] }
663
+ assert_equal ["Unauthorized IntegerObject: 1"], res["errors"].map { |e| e["message"] }
664
+ end
665
+
666
+ it "works with lazy values / interfaces" do
667
+ query = <<-GRAPHQL
668
+ query($value: String!){
669
+ unauthorizedInterface(value: $value) {
670
+ ... on UnauthorizedCheckBox {
671
+ value
672
+ }
673
+ }
674
+ }
675
+ GRAPHQL
676
+
677
+ res = auth_execute(query, variables: { value: "a"})
678
+ assert_nil res["data"]["unauthorizedInterface"]
679
+
680
+ res2 = auth_execute(query, variables: { value: "b"})
681
+ assert_equal "b", res2["data"]["unauthorizedInterface"]["value"]
682
+ end
683
+ end
684
+ end