File: authorization_spec.rb

package info (click to toggle)
ruby-graphql 2.2.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,584 kB
  • sloc: ruby: 67,505; ansic: 1,753; yacc: 831; javascript: 331; makefile: 6
file content (957 lines) | stat: -rw-r--r-- 32,075 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
# frozen_string_literal: true
require "spec_helper"

describe "GraphQL::Authorization" do
  module AuthTest
    class Box
      attr_reader :value
      def initialize(value:)
        @value = value
      end
    end

    class BaseArgument < GraphQL::Schema::Argument
      def visible?(context)
        super && (context[:hide] ? @name != "hidden" : true)
      end

      def authorized?(parent_object, value, context)
        super && parent_object != :hide2
      end
    end

    class BaseInputObjectArgument < BaseArgument
      def authorized?(parent_object, value, context)
        super && parent_object != :hide3
      end
    end

    class BaseInputObject < GraphQL::Schema::InputObject
      argument_class BaseInputObjectArgument
    end

    class BaseField < GraphQL::Schema::Field
      argument_class BaseArgument
      def visible?(context)
        super && (context[:hide] ? @name != "hidden" : true)
      end

      def authorized?(object, args, context)
        if object == :raise
          raise GraphQL::UnauthorizedFieldError.new("raised authorized field error", object: object)
        end
        return Box.new(value: context[:lazy_field_authorized]) if context.key?(:lazy_field_authorized)

        super && object != :hide && object != :replace
      end
    end

    class BaseObject < GraphQL::Schema::Object
      field_class BaseField
    end

    module BaseInterface
      include GraphQL::Schema::Interface
    end

    class BaseEnumValue < GraphQL::Schema::EnumValue
      def initialize(*args, role: nil, **kwargs)
        @role = role
        super(*args, **kwargs)
      end

      def visible?(context)
        super && (context[:hide] ? @role != :hidden : true)
      end
    end

    class BaseEnum < GraphQL::Schema::Enum
      enum_value_class(BaseEnumValue)
    end

    module HiddenInterface
      include BaseInterface

      def self.visible?(ctx)
        super && !ctx[:hide]
      end

      def self.resolve_type(obj, ctx)
        HiddenObject
      end
    end

    module HiddenDefaultInterface
      include BaseInterface
      # visible? will call the super method
      def self.resolve_type(obj, ctx)
        HiddenObject
      end
    end

    class HiddenObject < BaseObject
      implements HiddenInterface
      implements HiddenDefaultInterface
      def self.visible?(ctx)
        super && !ctx[:hide]
      end

      field :some_field, String
    end

    class RelayObject < BaseObject
      def self.visible?(ctx)
        super && !ctx[:hidden_relay]
      end

      def self.authorized?(_val, ctx)
        super && !ctx[:unauthorized_relay]
      end

      field :some_field, String
    end

    class UnauthorizedObject < BaseObject
      def self.authorized?(value, context)
        if context[:raise]
          raise GraphQL::UnauthorizedError.new("raised authorized object error", object: value.object)
        end
        super && !context[:hide]
      end

      field :value, String, null: false, method: :itself
    end

    class UnauthorizedBox < BaseObject
      # Hide `"a"`
      def self.authorized?(value, context)
        super && value != "a"
      end

      field :value, String, null: false, method: :itself
    end

    module UnauthorizedInterface
      include BaseInterface

      def self.resolve_type(obj, ctx)
        if obj.is_a?(String)
          UnauthorizedCheckBox
        else
          raise "Unexpected value: #{obj.inspect}"
        end
      end
    end

    class UnauthorizedCheckBox < BaseObject
      implements UnauthorizedInterface
      # This authorized check returns a lazy object, it should be synced by the runtime.
      def self.authorized?(value, context)
        if !value.is_a?(String)
          raise "Unexpected box value: #{value.inspect}"
        end
        is_authed = super && value != "a"
        # Make it many levels nested just to make sure we support nested lazy objects
        Box.new(value: Box.new(value: Box.new(value: Box.new(value: is_authed))))
      end

      field :value, String, null: false, method: :itself
    end

    class IntegerObject < BaseObject
      def self.authorized?(obj, ctx)
        if !obj.is_a?(Integer)
          raise "Unexpected IntegerObject: #{obj}"
        end
        is_allowed = !(ctx[:unauthorized_relay] || obj == ctx[:exclude_integer])
        Box.new(value: Box.new(value: is_allowed))
      end
      field :value, Integer, null: false, method: :itself
    end

    class IntegerObjectEdge < GraphQL::Types::Relay::BaseEdge
      node_type(IntegerObject)
    end

    class IntegerObjectConnection < GraphQL::Types::Relay::BaseConnection
      edge_type(IntegerObjectEdge)
    end

    # This object responds with `replaced => false`,
    # but if its replacement value is used, it gives `replaced => true`
    class Replaceable
      def replacement
        { replaced: true }
      end

      def replaced
        false
      end
    end

    class ReplacedObject < BaseObject
      def self.authorized?(obj, ctx)
        super && !ctx[:replace_me]
      end

      field :replaced, Boolean, null: false
    end

    class LandscapeFeature < BaseEnum
      value "MOUNTAIN"
      value "STREAM", role: :unauthorized
      value "FIELD"
      value "TAR_PIT", role: :hidden
    end

    class Query < BaseObject
      def self.authorized?(obj, ctx)
        !ctx[:query_unauthorized]
      end

      field :hidden, Integer, null: false
      field :unauthorized, Integer, method: :itself
      field :int2, Integer do
        argument :int, Integer, required: false
        argument :hidden, Integer, required: false
        argument :unauthorized, Integer, required: false
      end

      def int2(**args)
        args[:unauthorized] || 1
      end

      field :landscape_feature, LandscapeFeature, null: false do
        argument :string, String, required: false
        argument :enum, LandscapeFeature, required: false
      end

      def landscape_feature(string: nil, enum: nil)
        string || enum
      end

      field :landscape_features, [LandscapeFeature], null: false do
        argument :strings, [String], required: false
        argument :enums, [LandscapeFeature], required: false
      end

      def landscape_features(strings: [], enums: [])
        strings + enums
      end

      def empty_array; []; end
      field :hidden_object, HiddenObject, null: false, resolver_method: :itself
      field :hidden_interface, HiddenInterface, null: false, resolver_method: :itself
      field :hidden_default_interface, HiddenDefaultInterface, null: false, resolver_method: :itself
      field :hidden_connection, RelayObject.connection_type, null: :false, resolver_method: :empty_array
      field :hidden_edge, RelayObject.edge_type, null: :false, resolver_method: :edge_object

      field :unauthorized_object, UnauthorizedObject, resolver_method: :itself
      field :unauthorized_connection, RelayObject.connection_type, null: false, resolver_method: :array_with_item
      field :unauthorized_edge, RelayObject.edge_type, null: false, resolver_method: :edge_object

      def edge_object
        OpenStruct.new(node: 100)
      end

      def array_with_item
        [1]
      end

      field :unauthorized_lazy_box, UnauthorizedBox do
        argument :value, String
      end
      def unauthorized_lazy_box(value:)
        # Make it extra nested, just for good measure.
        Box.new(value: Box.new(value: value))
      end
      field :unauthorized_list_items, [UnauthorizedObject]
      def unauthorized_list_items
        [self, self]
      end

      field :unauthorized_lazy_check_box, UnauthorizedCheckBox, resolver_method: :unauthorized_lazy_box do
        argument :value, String
      end

      field :unauthorized_interface, UnauthorizedInterface, resolver_method: :unauthorized_lazy_box do
        argument :value, String
      end

      field :unauthorized_lazy_list_interface, [UnauthorizedInterface, null: true]

      def unauthorized_lazy_list_interface
        ["z", Box.new(value: Box.new(value: "z2")), "a", Box.new(value: "a")]
      end

      field :integers, IntegerObjectConnection, null: false

      def integers
        [1,2,3]
      end

      field :lazy_integers, IntegerObjectConnection, null: false

      def lazy_integers
        Box.new(value: Box.new(value: [1,2,3]))
      end

      field :replaced_object, ReplacedObject, null: false
      def replaced_object
        Replaceable.new
      end
    end

    class DoHiddenStuff < GraphQL::Schema::RelayClassicMutation
      def self.visible?(ctx)
        super && (ctx[:hidden_mutation] ? false : true)
      end
    end

    class DoHiddenStuff2 < GraphQL::Schema::Mutation
      def self.visible?(ctx)
        super && !ctx[:hidden_mutation]
      end

      field :some_return_field, String
    end

    class DoUnauthorizedStuff < GraphQL::Schema::RelayClassicMutation
      def self.authorized?(obj, ctx)
        super && (ctx[:unauthorized_mutation] ? false : true)
      end
    end

    class Mutation < BaseObject
      field :do_hidden_stuff, mutation: DoHiddenStuff
      field :do_hidden_stuff2, mutation: DoHiddenStuff2
      field :do_unauthorized_stuff, mutation: DoUnauthorizedStuff
    end

    class Nothing < GraphQL::Schema::Directive
      locations(FIELD)
      def self.visible?(ctx)
        !!ctx[:show_nothing_directive]
      end
    end

    class Schema < GraphQL::Schema
      query(Query)
      mutation(Mutation)
      directive(Nothing)

      lazy_resolve(Box, :value)

      def self.unauthorized_object(err)
        if err.object.respond_to?(:replacement)
          err.object.replacement
        elsif err.object == :replace
          33
        elsif err.object == :raise_from_object
          raise GraphQL::ExecutionError, err.message
        else
          raise GraphQL::ExecutionError, "Unauthorized #{err.type.graphql_name}: #{err.object.inspect}"
        end
      end

      # use GraphQL::Backtrace
    end

    class SchemaWithFieldHook < GraphQL::Schema
      query(Query)

      lazy_resolve(Box, :value)

      def self.unauthorized_field(err)
        if err.object == :replace
          42
        elsif err.object == :raise
          raise GraphQL::ExecutionError, "#{err.message} in field #{err.field.graphql_name}"
        else
          raise GraphQL::ExecutionError, "Unauthorized field #{err.field.graphql_name} on #{err.type.graphql_name}: #{err.object}"
        end
      end
    end
  end

  def auth_execute(*args, **kwargs)
    AuthTest::Schema.execute(*args, **kwargs)
  end

  describe "applying the visible? method" do
    it "works in queries" do
      res = auth_execute(" { int int2 } ", context: { hide: true })
      assert_equal 1, res["errors"].size
    end

    it "applies return type visibility to fields" do
      error_queries = {
        "hiddenObject" => "{ hiddenObject { __typename } }",
        "hiddenInterface" => "{ hiddenInterface { __typename } }",
        "hiddenDefaultInterface" => "{ hiddenDefaultInterface { __typename } }",
      }

      error_queries.each do |name, q|
        hidden_res = auth_execute(q, context: { hide: true})
        assert_equal ["Field '#{name}' doesn't exist on type 'Query'"], hidden_res["errors"].map { |e| e["message"] }

        visible_res = auth_execute(q)
        # Both fields exist; the interface resolves to the object type, though
        assert_equal "HiddenObject", visible_res["data"][name]["__typename"]
      end
    end

    it "uses the mutation for derived fields, inputs and outputs" do
      query = "mutation { doHiddenStuff(input: {}) { __typename } }"
      res = auth_execute(query, context: { hidden_mutation: true })
      assert_equal ["Field 'doHiddenStuff' doesn't exist on type 'Mutation'"], res["errors"].map { |e| e["message"] }

      # `#resolve` isn't implemented, so this errors out:
      assert_raises GraphQL::RequiredImplementationMissingError do
        auth_execute(query)
      end

      introspection_q = <<-GRAPHQL
        {
          t1: __type(name: "DoHiddenStuffInput") { name }
          t2: __type(name: "DoHiddenStuffPayload") { name }
        }
      GRAPHQL
      hidden_introspection_res = auth_execute(introspection_q, context: { hidden_mutation: true })
      assert_nil hidden_introspection_res["data"]["t1"]
      assert_nil hidden_introspection_res["data"]["t2"]

      visible_introspection_res = auth_execute(introspection_q)
      assert_equal "DoHiddenStuffInput", visible_introspection_res["data"]["t1"]["name"]
      assert_equal "DoHiddenStuffPayload", visible_introspection_res["data"]["t2"]["name"]
    end

    it "works with Schema::Mutation" do
      query = "mutation { doHiddenStuff2 { __typename } }"
      res = auth_execute(query, context: { hidden_mutation: true })
      assert_equal ["Field 'doHiddenStuff2' doesn't exist on type 'Mutation'"], res["errors"].map { |e| e["message"] }

      # `#resolve` isn't implemented, so this errors out:
      assert_raises GraphQL::RequiredImplementationMissingError do
        auth_execute(query)
      end
    end

    it "uses the base type for edges and connections" do
      query = <<-GRAPHQL
      {
        hiddenConnection { __typename }
        hiddenEdge { __typename }
      }
      GRAPHQL

      hidden_res = auth_execute(query, context: { hidden_relay: true })
      assert_equal 2, hidden_res["errors"].size

      visible_res = auth_execute(query)
      assert_equal "RelayObjectConnection", visible_res["data"]["hiddenConnection"]["__typename"]
      assert_equal "RelayObjectEdge", visible_res["data"]["hiddenEdge"]["__typename"]
    end

    it "treats hidden enum values as non-existant, even in lists" do
      hidden_res_1 = auth_execute <<-GRAPHQL, context: { hide: true }
      {
        landscapeFeature(enum: TAR_PIT)
      }
      GRAPHQL

      assert_equal ["Argument 'enum' on Field 'landscapeFeature' has an invalid value (TAR_PIT). Expected type 'LandscapeFeature'."], hidden_res_1["errors"].map { |e| e["message"] }

      hidden_res_2 = auth_execute <<-GRAPHQL, context: { hide: true }
      {
        landscapeFeatures(enums: [STREAM, TAR_PIT])
      }
      GRAPHQL

      assert_equal ["Argument 'enums' on Field 'landscapeFeatures' has an invalid value ([STREAM, TAR_PIT]). Expected type '[LandscapeFeature!]'."], hidden_res_2["errors"].map { |e| e["message"] }

      success_res = auth_execute <<-GRAPHQL, context: { hide: false }
      {
        landscapeFeature(enum: TAR_PIT)
        landscapeFeatures(enums: [STREAM, TAR_PIT])
      }
      GRAPHQL

      assert_equal "TAR_PIT", success_res["data"]["landscapeFeature"]
      assert_equal ["STREAM", "TAR_PIT"], success_res["data"]["landscapeFeatures"]
    end

    it "refuses to resolve to hidden enum values" do
      expected_class = AuthTest::LandscapeFeature::UnresolvedValueError
      assert_raises(expected_class) do
        auth_execute <<-GRAPHQL, context: { hide: true }
        {
          landscapeFeature(string: "TAR_PIT")
        }
        GRAPHQL
      end

      assert_raises(expected_class) do
        auth_execute <<-GRAPHQL, context: { hide: true }
        {
          landscapeFeatures(strings: ["STREAM", "TAR_PIT"])
        }
        GRAPHQL
      end
    end

    it "works in introspection" do
      res = auth_execute <<-GRAPHQL, context: { hide: true, hidden_mutation: true }
        {
          query: __type(name: "Query") {
            fields {
              name
              args { name }
            }
          }

          hiddenObject: __type(name: "HiddenObject") { name }
          hiddenInterface: __type(name: "HiddenInterface") { name }
          landscapeFeatures: __type(name: "LandscapeFeature") { enumValues { name } }
        }
      GRAPHQL
      query_field_names = res["data"]["query"]["fields"].map { |f| f["name"] }
      refute_includes query_field_names, "int"
      int2_arg_names = res["data"]["query"]["fields"].find { |f| f["name"] == "int2" }["args"].map { |a| a["name"] }
      assert_equal ["int", "unauthorized"], int2_arg_names

      assert_nil res["data"]["hiddenObject"]
      assert_nil res["data"]["hiddenInterface"]

      visible_landscape_features = res["data"]["landscapeFeatures"]["enumValues"].map { |v| v["name"] }
      assert_equal ["MOUNTAIN", "STREAM", "FIELD"], visible_landscape_features
    end

    it "works when printing the SDL" do
      full_sdl = AuthTest::Schema.to_definition
      restricted_sdl = AuthTest::Schema.to_definition(context: { hide: true, hidden_mutation: true, hidden_relay: true })
      assert_includes full_sdl, 'Hidden'
      assert_includes full_sdl, 'hidden'
      refute_includes restricted_sdl, 'Hidden'
      refute_includes restricted_sdl, 'hidden'
    end

    it "works with directives" do
      query_str = "{ __typename @nothing }"
      visible_response = auth_execute(query_str, context: { show_nothing_directive: true })
      assert_equal "Query", visible_response["data"]["__typename"]
      hidden_response = auth_execute(query_str)
      assert_equal ["Directive @nothing is not defined"], hidden_response["errors"].map { |e| e["message"] }
    end
  end

  describe "applying the authorized? method" do
    it "halts on unauthorized objects, replacing the object with nil" do
      query = "{ unauthorizedObject { __typename } }"
      hidden_response = auth_execute(query, context: { hide: true })
      assert_nil hidden_response["data"].fetch("unauthorizedObject")
      visible_response = auth_execute(query, context: {})
      assert_equal({ "__typename" => "UnauthorizedObject" }, visible_response["data"]["unauthorizedObject"])
    end

    it "halts on unauthorized mutations" do
      query = "mutation { doUnauthorizedStuff(input: {}) { __typename } }"
      res = auth_execute(query, context: { unauthorized_mutation: true })
      assert_nil res["data"].fetch("doUnauthorizedStuff")
      assert_raises GraphQL::RequiredImplementationMissingError do
        auth_execute(query)
      end
    end

    describe "field level authorization" do
      describe "unauthorized field" do
        describe "with an unauthorized field hook configured" do
          describe "when the hook returns a value" do
            it "replaces the response with the return value of the unauthorized field hook" do
              query = "{ unauthorized }"
              response = AuthTest::SchemaWithFieldHook.execute(query, root_value: :replace)
              assert_equal 42, response["data"].fetch("unauthorized")
            end
          end

          describe "when the field hook raises an error" do
            it "returns nil" do
              query = "{ unauthorized }"
              response = AuthTest::SchemaWithFieldHook.execute(query, root_value: :hide)
              assert_nil response["data"].fetch("unauthorized")
            end

            it "adds the error to the errors key" do
              query = "{ unauthorized }"
              response = AuthTest::SchemaWithFieldHook.execute(query, root_value: :hide)
              assert_equal ["Unauthorized field unauthorized on Query: hide"], response["errors"].map { |e| e["message"] }
            end
          end


          describe "when the field authorization resolves lazily" do
            it "returns value if authorized" do
              query = "{ unauthorized }"
              response = AuthTest::SchemaWithFieldHook.execute(query, root_value: 34, context: { lazy_field_authorized: true })
              assert_equal 34, response["data"].fetch("unauthorized")
            end

            it "returns nil if not authorized" do
              query = "{ unauthorized }"
              response = AuthTest::SchemaWithFieldHook.execute(query, root_value: 34, context: { lazy_field_authorized: false })
              assert_nil response["data"].fetch("unauthorized")
              assert_equal ["Unauthorized field unauthorized on Query: 34"], response["errors"].map { |e| e["message"] }
            end
          end

          describe "when the field authorization raises an UnauthorizedFieldError" do
            it "receives the raised error" do
              query = "{ unauthorized }"
              response = AuthTest::SchemaWithFieldHook.execute(query, root_value: :raise)
              assert_equal ["raised authorized field error in field unauthorized"], response["errors"].map { |e| e["message"] }
            end
          end
        end

        describe "with an unauthorized field hook not configured" do
          describe "When the object hook replaces the field" do
            it "delegates to the unauthorized object hook, which replaces the object" do
              query = "{ unauthorized }"
              response = AuthTest::Schema.execute(query, root_value: :replace)
              assert_equal 33, response["data"].fetch("unauthorized")
            end
          end
          describe "When the object hook raises an error" do
            it "returns nil" do
              query = "{ unauthorized }"
              response = AuthTest::Schema.execute(query, root_value: :hide)
              assert_nil response["data"].fetch("unauthorized")
            end

            it "adds the error to the errors key" do
              query = "{ unauthorized }"
              response = AuthTest::Schema.execute(query, root_value: :hide)
              assert_equal ["Unauthorized Query: :hide"], response["errors"].map { |e| e["message"] }
            end
          end
        end
      end

      describe "authorized field" do
        it "returns the field data" do
          query = "{ unauthorized }"
          response = AuthTest::SchemaWithFieldHook.execute(query, root_value: 1)
          assert_equal 1, response["data"].fetch("unauthorized")
        end
      end
    end

    it "halts on unauthorized fields, using the parent object" do
      query = "{ unauthorized }"
      hidden_response = auth_execute(query, root_value: :hide)
      assert_nil hidden_response["data"].fetch("unauthorized")
      visible_response = auth_execute(query, root_value: 1)
      assert_equal 1, visible_response["data"]["unauthorized"]
    end

    it "halts on unauthorized arguments, using the parent object" do
      query = "{ int2(unauthorized: 5) }"
      hidden_response = auth_execute(query, root_value: :hide2)
      assert_nil hidden_response["data"].fetch("int2")
      visible_response = auth_execute(query)
      assert_equal 5, visible_response["data"]["int2"]
    end

    it "works with edges and connections" do
      query = <<-GRAPHQL
      {
        unauthorizedConnection {
          __typename
          edges {
            __typename
            node {
              __typename
            }
          }
          nodes {
            __typename
          }
        }
        unauthorizedEdge {
          __typename
          node {
            __typename
          }
        }
      }
      GRAPHQL

      unauthorized_res = auth_execute(query, context: { unauthorized_relay: true })
      conn = unauthorized_res["data"].fetch("unauthorizedConnection")
      assert_equal "RelayObjectConnection", conn.fetch("__typename")
      # This is tricky: the previous behavior was to replace the _whole_
      # list with `nil`. This was due to an implementation detail:
      # The list field's return value (an array of integers) was wrapped
      # _before_ returning, and during this wrapping, a cascading error
      # caused the entire field to be nilled out.
      #
      # In the interpreter, each list item is contained and the error doesn't propagate
      # up to the whole list.
      #
      # Originally, I thought that this was a _feature_ that obscured list entries.
      # But really, look at the test below: you don't get this "feature" if
      # you use `edges { node }`, so it can't be relied on in any way.
      #
      # All that to say, in the interpreter, `nodes` and `edges { node }` behave
      # the same.
      #
      # TODO revisit the docs for this.
      failed_nodes_value = [nil]
      assert_equal failed_nodes_value, conn.fetch("nodes")
      assert_equal [{"node" => nil, "__typename" => "RelayObjectEdge"}], conn.fetch("edges")

      edge = unauthorized_res["data"].fetch("unauthorizedEdge")
      assert_nil edge.fetch("node")
      assert_equal "RelayObjectEdge", edge["__typename"]

      unauthorized_object_paths = [
        ["unauthorizedConnection", "edges", 0, "node"],
        ["unauthorizedConnection", "nodes", 0],
        ["unauthorizedEdge", "node"]
      ]

      assert_equal unauthorized_object_paths, unauthorized_res["errors"].map { |e| e["path"] }

      authorized_res = auth_execute(query)
      conn = authorized_res["data"].fetch("unauthorizedConnection")
      assert_equal "RelayObjectConnection", conn.fetch("__typename")
      assert_equal [{"__typename"=>"RelayObject"}], conn.fetch("nodes")
      assert_equal [{"node" => {"__typename" => "RelayObject"}, "__typename" => "RelayObjectEdge"}], conn.fetch("edges")

      edge = authorized_res["data"].fetch("unauthorizedEdge")
      assert_equal "RelayObject", edge.fetch("node").fetch("__typename")
      assert_equal "RelayObjectEdge", edge["__typename"]
    end

    it "authorizes _after_ resolving lazy objects" do
      query = <<-GRAPHQL
      {
        a: unauthorizedLazyBox(value: "a") { value }
        b: unauthorizedLazyBox(value: "b") { value }
      }
      GRAPHQL

      unauthorized_res = auth_execute(query)
      assert_nil unauthorized_res["data"].fetch("a")
      assert_equal "b", unauthorized_res["data"]["b"]["value"]
    end

    it "authorizes items in a list" do
      query = <<-GRAPHQL
      {
        unauthorizedListItems { __typename }
      }
      GRAPHQL

      unauthorized_res = auth_execute(query, context: { hide: true })

      assert_nil unauthorized_res["data"]["unauthorizedListItems"]
      authorized_res = auth_execute(query, context: { hide: false })
      assert_equal 2, authorized_res["data"]["unauthorizedListItems"].size
    end

    it "syncs lazy objects from authorized? checks" do
      query = <<-GRAPHQL
      {
        a: unauthorizedLazyCheckBox(value: "a") { value }
        b: unauthorizedLazyCheckBox(value: "b") { value }
      }
      GRAPHQL

      unauthorized_res = auth_execute(query)
      assert_nil unauthorized_res["data"].fetch("a")
      assert_equal "b", unauthorized_res["data"]["b"]["value"]
      # Also, the custom handler was called:
      assert_equal ["Unauthorized UnauthorizedCheckBox: \"a\""], unauthorized_res["errors"].map { |e| e["message"] }
    end

    it "Works for lazy connections" do
      query = <<-GRAPHQL
      {
        lazyIntegers { edges { node { value } } }
      }
      GRAPHQL
      res = auth_execute(query)
      assert_equal [1,2,3], res["data"]["lazyIntegers"]["edges"].map { |e| e["node"]["value"] }
    end

    it "Works for eager connections" do
      query = <<-GRAPHQL
      {
        integers { edges { node { value } } }
      }
      GRAPHQL
      res = auth_execute(query)
      assert_equal [1,2,3], res["data"]["integers"]["edges"].map { |e| e["node"]["value"] }
    end

    it "filters out individual nodes by value" do
      query = <<-GRAPHQL
      {
        integers { edges { node { value } } }
      }
      GRAPHQL
      res = auth_execute(query, context: { exclude_integer: 1 })
      assert_equal [nil,2,3], res["data"]["integers"]["edges"].map { |e| e["node"] && e["node"]["value"] }
      assert_equal ["Unauthorized IntegerObject: 1"], res["errors"].map { |e| e["message"] }
    end

    it "works with lazy values / interfaces" do
      query = <<-GRAPHQL
      query($value: String!){
        unauthorizedInterface(value: $value) {
          ... on UnauthorizedCheckBox {
            value
          }
        }
      }
      GRAPHQL

      res = auth_execute(query, variables: { value: "a"})
      assert_nil res["data"]["unauthorizedInterface"]

      res2 = auth_execute(query, variables: { value: "b"})
      assert_equal "b", res2["data"]["unauthorizedInterface"]["value"]
    end

    it "works with lazy values / lists of interfaces" do
      query = <<-GRAPHQL
      {
        unauthorizedLazyListInterface {
          ... on UnauthorizedCheckBox {
            value
          }
        }
      }
      GRAPHQL

      res = auth_execute(query)
      # An error from two, values from the others
      assert_equal ["Unauthorized UnauthorizedCheckBox: \"a\"", "Unauthorized UnauthorizedCheckBox: \"a\""], res["errors"].map { |e| e["message"] }
      assert_equal [{"value" => "z"}, {"value" => "z2"}, nil, nil], res["data"]["unauthorizedLazyListInterface"]
    end

    describe "with an unauthorized field hook configured" do
      it "replaces objects from the unauthorized_object hook" do
        query = "{ replacedObject { replaced } }"
        res = auth_execute(query, context: { replace_me: true })
        assert_equal true, res["data"]["replacedObject"]["replaced"]

        res = auth_execute(query, context: { replace_me: false })
        assert_equal false, res["data"]["replacedObject"]["replaced"]
      end

      it "works when the query hook returns false and there's no root object" do
        query = "{ __typename }"
        res = auth_execute(query)
        assert_equal "Query", res["data"]["__typename"]

        unauth_res = auth_execute(query, context: { query_unauthorized: true })
        assert_nil unauth_res["data"]
        assert_equal [{"message"=>"Unauthorized Query: nil"}], unauth_res["errors"]
      end

      describe "when the object authorization raises an UnauthorizedFieldError" do
        it "receives the raised error" do
          query = "{ unauthorizedObject { value } }"
          response = auth_execute(query, context: { raise: true }, root_value: :raise_from_object)
          assert_equal ["raised authorized object error"], response["errors"].map { |e| e["message"] }
        end
      end
    end
  end

  describe "returning false" do
    class FalseSchema < GraphQL::Schema
      class Query < GraphQL::Schema::Object
        def self.authorized?(obj, ctx)
          false
        end

        field :int, Integer, null: false

        def int
          1
        end
      end
      query(Query)
    end

    it "works out-of-the-box" do
      res = FalseSchema.execute("{ int }")
      assert_nil res.fetch("data")
      refute res.key?("errors")
    end
  end

  describe "overriding authorized_new" do
    class AuthorizedNewOverrideSchema < GraphQL::Schema
      module LogTrace
        def trace(key, data)
          if ((q = data[:query]) && (c = q.context))
            c[:log] << key
          end
          yield
        end
        ["parse", "lex", "validate",
        "analyze_query", "analyze_multiplex",
        "execute_query", "execute_multiplex",
        "execute_field", "execute_field_lazy",
        "authorized", "authorized_lazy",
        "resolve_type", "resolve_type_lazy",
        "execute_query_lazy"].each do |method_name|
          define_method(method_name) do |**data, &block|
            trace(method_name, data, &block)
          end
        end
      end

      module CustomIntrospection
        class DynamicFields < GraphQL::Introspection::DynamicFields
          def self.authorized_new(obj, ctx)
            new(obj, ctx)
          end
        end
      end

      class Query < GraphQL::Schema::Object
        def self.authorized_new(obj, ctx)
          new(obj, ctx)
        end
        field :int, Integer, null: false
        def int; 1; end
      end

      query(Query)
      introspection(CustomIntrospection)
      trace_with(LogTrace)
    end

    it "avoids calls to Object.authorized?" do
      log = []
      res = AuthorizedNewOverrideSchema.execute("{ __typename int }", context: { log: log })
      assert_equal "Query", res["data"]["__typename"]
      assert_equal 1, res["data"]["int"]
      expected_log = [
        "validate",
        "analyze_query",
        "execute_query",
        "execute_field",
        "execute_field",
        "execute_query_lazy"
      ]

      assert_equal expected_log, log
    end
  end
end