From c033964e91e76a9cdf659400d3cac8094dfef4fe Mon Sep 17 00:00:00 2001 From: Marc-Andre Giroux Date: Tue, 23 Oct 2018 14:48:32 -0400 Subject: [PATCH 1/3] Use new analysis engine when using Interpreter --- guides/queries/ast_analysis.md | 4 ++-- guides/queries/interpreter.md | 3 +++ lib/graphql/analysis/ast.rb | 5 ----- lib/graphql/execution/interpreter.rb | 2 ++ spec/graphql/analysis/ast_spec.rb | 2 +- .../rules/variables_are_used_and_defined_spec.rb | 1 - 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/guides/queries/ast_analysis.md b/guides/queries/ast_analysis.md index c0bdc7a768..0b32bd0dc6 100644 --- a/guides/queries/ast_analysis.md +++ b/guides/queries/ast_analysis.md @@ -125,11 +125,11 @@ end ### Using Analyzers -The new query analyzers are added to the schema the same one as before with `query_analyzer`. However, to use the new analysis engine, you must opt in by using `use GraphQL::Analysis::AST`, for example: +The new query analyzers are added to the schema the same one as before with `query_analyzer`. However, to use the new analysis engine, you must opt in by using the new runtime module, [GraphQL::Execution::Interpreter](guides/queries/interpreter.md), for example: ```ruby class MySchema < GraphQL::Schema - use GraphQL::Analysis::AST + use GraphQL::Execution::Interpreter query_analyzer MyQueryAnalyzer end ``` diff --git a/guides/queries/interpreter.md b/guides/queries/interpreter.md index 895c06eb8a..9721d38322 100644 --- a/guides/queries/interpreter.md +++ b/guides/queries/interpreter.md @@ -73,6 +73,9 @@ The new runtime works with class-based schemas only. Several features are no lon These depend on the now-removed `Rewrite` step, which wasted a lot of time making often-unneeded preparation. Most of the attributes you might need from an `irep_node` are available with `extras: [...]`. Query analyzers can be refactored to be static checks (custom validation rules) or dynamic checks, made at runtime. The built-in analyzers have been refactored to run as validators. + A new style of analyzers has been added, which rely only on the query AST. By using the interpreter, you automatically opt-in + into the new analysis runtime, which is [described here](guides/queries/ast_analysis.md). + `irep_node`-based lookahead is not supported. Stay tuned for a replacement. - `rescue_from` diff --git a/lib/graphql/analysis/ast.rb b/lib/graphql/analysis/ast.rb index 7b049d2403..f1a12cadc0 100644 --- a/lib/graphql/analysis/ast.rb +++ b/lib/graphql/analysis/ast.rb @@ -13,11 +13,6 @@ module Analysis module AST module_function - def use(schema_defn) - schema = schema_defn.target - schema.analysis_engine = GraphQL::Analysis::AST - end - # Analyze a multiplex, and all queries within. # Multiplex analyzers are ran for all queries, keeping state. # Query analyzers are ran per query, without carrying state between queries. diff --git a/lib/graphql/execution/interpreter.rb b/lib/graphql/execution/interpreter.rb index 7c9318c5f3..3922f8b817 100644 --- a/lib/graphql/execution/interpreter.rb +++ b/lib/graphql/execution/interpreter.rb @@ -22,6 +22,8 @@ def self.use(schema_defn) schema_defn.query_execution_strategy(GraphQL::Execution::Interpreter) schema_defn.mutation_execution_strategy(GraphQL::Execution::Interpreter) schema_defn.subscription_execution_strategy(GraphQL::Execution::Interpreter) + schema = schema_defn.target + schema.analysis_engine = GraphQL::Analysis::AST end def self.begin_multiplex(multiplex) diff --git a/spec/graphql/analysis/ast_spec.rb b/spec/graphql/analysis/ast_spec.rb index 76126cdd99..6b2c0ee627 100644 --- a/spec/graphql/analysis/ast_spec.rb +++ b/spec/graphql/analysis/ast_spec.rb @@ -75,7 +75,7 @@ def foobar Class.new(GraphQL::Schema) do query query_type - use GraphQL::Analysis::AST + use GraphQL::Execution::Interpreter query_analyzer AstErrorAnalyzer end end diff --git a/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb b/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb index 1b617cc493..9f63f4082d 100644 --- a/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb +++ b/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb @@ -71,7 +71,6 @@ GRAPHQL } - focus it "finds usages" do assert_equal([], errors) end From 08f72cfb1b90ecff7cf15c7c8a2cfc7bd8d9549c Mon Sep 17 00:00:00 2001 From: Marc-Andre Giroux Date: Wed, 24 Oct 2018 11:39:10 -0400 Subject: [PATCH 2/3] fix tests --- lib/graphql/execution/multiplex.rb | 6 +- spec/graphql/analysis/analyze_query_spec.rb | 47 +- spec/graphql/authorization_spec.rb | 1276 ++++++++++--------- 3 files changed, 675 insertions(+), 654 deletions(-) diff --git a/lib/graphql/execution/multiplex.rb b/lib/graphql/execution/multiplex.rb index c66ffd1127..55a57049b0 100644 --- a/lib/graphql/execution/multiplex.rb +++ b/lib/graphql/execution/multiplex.rb @@ -174,7 +174,11 @@ def instrument_and_analyze(multiplex) schema = multiplex.schema multiplex_analyzers = schema.multiplex_analyzers if multiplex.max_complexity - multiplex_analyzers += [GraphQL::Analysis::MaxQueryComplexity.new(multiplex.max_complexity)] + if schema.using_ast_analysis? + multiplex_analyzers += [GraphQL::Analysis::AST::MaxQueryComplexity] + else + multiplex_analyzers += [GraphQL::Analysis::MaxQueryComplexity.new(multiplex.max_complexity)] + end end schema.analysis_engine.analyze_multiplex(multiplex, multiplex_analyzers) diff --git a/spec/graphql/analysis/analyze_query_spec.rb b/spec/graphql/analysis/analyze_query_spec.rb index 45afe83ecb..718970a6fd 100644 --- a/spec/graphql/analysis/analyze_query_spec.rb +++ b/spec/graphql/analysis/analyze_query_spec.rb @@ -2,6 +2,21 @@ require "spec_helper" describe GraphQL::Analysis do + class ClassicAnalysisSchema < GraphQL::Schema + query Dummy::DairyAppQuery + mutation Dummy::DairyAppMutation + subscription Dummy::Subscription + max_depth 5 + # TODO why is `.graphql_definition` required here? + orphan_types Dummy::Honey, Dummy::Beverage.graphql_definition + + rescue_from(Dummy::NoSuchDairyError) { |err| err.message } + + def self.resolve_type(type, obj, ctx) + ClassicAnalysisSchema.types[obj.class.name.split("::").last] + end + end + class TypeCollector def initial_value(query) [] @@ -43,7 +58,7 @@ def call(memo, visit_type, irep_node) let(:analyzers) { [type_collector, node_counter] } let(:reduce_result) { GraphQL::Analysis.analyze_query(query, analyzers) } let(:variables) { {} } - let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables) } + let(:query) { GraphQL::Query.new(ClassicAnalysisSchema, query_string, variables: variables) } let(:query_string) {%| { cheese(id: 1) { @@ -58,7 +73,7 @@ def call(memo, visit_type, irep_node) let(:analyzers) { [type_collector, conditional_analyzer] } describe "when analyze? returns false" do - let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables, context: { analyze: false }) } + let(:query) { GraphQL::Query.new(ClassicAnalysisSchema, query_string, variables: variables, context: { analyze: false }) } it "does not run the analyzer" do # Only type_collector ran @@ -67,7 +82,7 @@ def call(memo, visit_type, irep_node) end describe "when analyze? returns true" do - let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables, context: { analyze: true }) } + let(:query) { GraphQL::Query.new(ClassicAnalysisSchema, query_string, variables: variables, context: { analyze: true }) } it "it runs the analyzer" do # Both analyzers ran @@ -93,7 +108,7 @@ def call(memo, visit_type, irep_node) it "emits traces" do traces = TestTracing.with_trace do ctx = { tracers: [TestTracing] } - Dummy::Schema.execute(query_string, context: ctx) + ClassicAnalysisSchema.execute(query_string, context: ctx) end # The query_trace is on the list _first_ because it finished first @@ -119,14 +134,14 @@ def call(memo, visit_type, irep_node) let(:variable_accessor) { ->(memo, visit_type, irep_node) { query.variables["cheeseId"] } } before do - @previous_query_analyzers = Dummy::Schema.query_analyzers.dup - Dummy::Schema.query_analyzers.clear - Dummy::Schema.query_analyzers << variable_accessor + @previous_query_analyzers = ClassicAnalysisSchema.query_analyzers.dup + ClassicAnalysisSchema.query_analyzers.clear + ClassicAnalysisSchema.query_analyzers << variable_accessor end after do - Dummy::Schema.query_analyzers.clear - Dummy::Schema.query_analyzers.push(*@previous_query_analyzers) + ClassicAnalysisSchema.query_analyzers.clear + ClassicAnalysisSchema.query_analyzers.push(*@previous_query_analyzers) end it "returns an error" do @@ -213,7 +228,7 @@ def final_value(memo) let(:flavor_catcher) { FlavorCatcher.new } let(:analyzers) { [id_catcher, flavor_catcher] } let(:reduce_result) { GraphQL::Analysis.analyze_query(query, analyzers) } - let(:query) { GraphQL::Query.new(Dummy::Schema, query_string) } + let(:query) { GraphQL::Query.new(ClassicAnalysisSchema, query_string) } let(:query_string) {%| { cheese(id: 1) { @@ -222,7 +237,7 @@ def final_value(memo) } } |} - let(:schema) { Dummy::Schema } + let(:schema) { ClassicAnalysisSchema } let(:result) { schema.execute(query_string) } let(:query_string) {%| { @@ -234,14 +249,14 @@ def final_value(memo) |} before do - @previous_query_analyzers = Dummy::Schema.query_analyzers.dup - Dummy::Schema.query_analyzers.clear - Dummy::Schema.query_analyzers << id_catcher << flavor_catcher + @previous_query_analyzers = ClassicAnalysisSchema.query_analyzers.dup + ClassicAnalysisSchema.query_analyzers.clear + ClassicAnalysisSchema.query_analyzers << id_catcher << flavor_catcher end after do - Dummy::Schema.query_analyzers.clear - Dummy::Schema.query_analyzers.push(*@previous_query_analyzers) + ClassicAnalysisSchema.query_analyzers.clear + ClassicAnalysisSchema.query_analyzers.push(*@previous_query_analyzers) end it "groups all errors together" do diff --git a/spec/graphql/authorization_spec.rb b/spec/graphql/authorization_spec.rb index 5e708dfcde..fc35160592 100644 --- a/spec/graphql/authorization_spec.rb +++ b/spec/graphql/authorization_spec.rb @@ -1,825 +1,827 @@ # frozen_string_literal: true require "spec_helper" -describe GraphQL::Authorization do - module AuthTest - class Box - attr_reader :value - def initialize(value:) - @value = value +if !TESTING_INTERPRETER + describe GraphQL::Authorization do + module AuthTest + class Box + attr_reader :value + def initialize(value:) + @value = value + end end - end - class BaseArgument < GraphQL::Schema::Argument - def visible?(context) - super && (context[:hide] ? @name != "hidden" : true) - end + class BaseArgument < GraphQL::Schema::Argument + def visible?(context) + super && (context[:hide] ? @name != "hidden" : true) + end - def accessible?(context) - super && (context[:hide] ? @name != "inaccessible" : true) - end + def accessible?(context) + super && (context[:hide] ? @name != "inaccessible" : true) + end - def authorized?(parent_object, context) - super && parent_object != :hide2 + def authorized?(parent_object, context) + super && parent_object != :hide2 + end end - end - class BaseField < GraphQL::Schema::Field - def initialize(*args, edge_class: nil, **kwargs, &block) - @edge_class = edge_class - super(*args, **kwargs, &block) - end + class BaseField < GraphQL::Schema::Field + def initialize(*args, edge_class: nil, **kwargs, &block) + @edge_class = edge_class + super(*args, **kwargs, &block) + end - def to_graphql - field_defn = super - if @edge_class - field_defn.edge_class = @edge_class + def to_graphql + field_defn = super + if @edge_class + field_defn.edge_class = @edge_class + end + field_defn end - field_defn - end - argument_class BaseArgument - def visible?(context) - super && (context[:hide] ? @name != "hidden" : true) - end + argument_class BaseArgument + def visible?(context) + super && (context[:hide] ? @name != "hidden" : true) + end + + def accessible?(context) + super && (context[:hide] ? @name != "inaccessible" : true) + end - def accessible?(context) - super && (context[:hide] ? @name != "inaccessible" : true) + def authorized?(object, context) + super && object != :hide + end end - def authorized?(object, context) - super && object != :hide + class BaseObject < GraphQL::Schema::Object + field_class BaseField end - end - class BaseObject < GraphQL::Schema::Object - field_class BaseField - end + module BaseInterface + include GraphQL::Schema::Interface + 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 - class BaseEnumValue < GraphQL::Schema::EnumValue - def initialize(*args, role: nil, **kwargs) - @role = role - super(*args, **kwargs) + def visible?(context) + super && (context[:hide] ? @role != :hidden : true) + end end - def visible?(context) - super && (context[:hide] ? @role != :hidden : true) + class BaseEnum < GraphQL::Schema::Enum + enum_value_class(BaseEnumValue) end - end - class BaseEnum < GraphQL::Schema::Enum - enum_value_class(BaseEnumValue) - end + module HiddenInterface + include BaseInterface - module HiddenInterface - include BaseInterface + def self.visible?(ctx) + super && !ctx[:hide] + end - def self.visible?(ctx) - super && !ctx[:hide] + def self.resolve_type(obj, ctx) + HiddenObject + end end - def self.resolve_type(obj, ctx) - HiddenObject + module HiddenDefaultInterface + include BaseInterface + # visible? will call the super method + def self.resolve_type(obj, ctx) + HiddenObject + end end - end - module HiddenDefaultInterface - include BaseInterface - # visible? will call the super method - def self.resolve_type(obj, ctx) - HiddenObject + class HiddenObject < BaseObject + implements HiddenInterface + implements HiddenDefaultInterface + def self.visible?(ctx) + super && !ctx[:hide] + end end - end - class HiddenObject < BaseObject - implements HiddenInterface - implements HiddenDefaultInterface - def self.visible?(ctx) - super && !ctx[:hide] - end - end + class RelayObject < BaseObject + def self.visible?(ctx) + super && !ctx[:hidden_relay] + end - class RelayObject < BaseObject - def self.visible?(ctx) - super && !ctx[:hidden_relay] - end + def self.accessible?(ctx) + super && !ctx[:inaccessible_relay] + end - def self.accessible?(ctx) - super && !ctx[:inaccessible_relay] + def self.authorized?(_val, ctx) + super && !ctx[:unauthorized_relay] + end end - def self.authorized?(_val, ctx) - super && !ctx[:unauthorized_relay] - end - end + # TODO test default behavior for abstract types, + # that they check their concrete types + module InaccessibleInterface + include BaseInterface - # TODO test default behavior for abstract types, - # that they check their concrete types - module InaccessibleInterface - include BaseInterface + def self.accessible?(ctx) + super && !ctx[:hide] + end - def self.accessible?(ctx) - super && !ctx[:hide] + def self.resolve_type(obj, ctx) + InaccessibleObject + end end - def self.resolve_type(obj, ctx) - InaccessibleObject + module InaccessibleDefaultInterface + include BaseInterface + # accessible? will call the super method + def self.resolve_type(obj, ctx) + InaccessibleObject + end end - end - module InaccessibleDefaultInterface - include BaseInterface - # accessible? will call the super method - def self.resolve_type(obj, ctx) - InaccessibleObject + class InaccessibleObject < BaseObject + implements InaccessibleInterface + implements InaccessibleDefaultInterface + def self.accessible?(ctx) + super && !ctx[:hide] + end end - end - class InaccessibleObject < BaseObject - implements InaccessibleInterface - implements InaccessibleDefaultInterface - def self.accessible?(ctx) - super && !ctx[:hide] + class UnauthorizedObject < BaseObject + def self.authorized?(value, context) + super && !context[:hide] + end end - end - class UnauthorizedObject < BaseObject - def self.authorized?(value, context) - super && !context[:hide] - end - end + class UnauthorizedBox < BaseObject + # Hide `"a"` + def self.authorized?(value, context) + super && value != "a" + end - class UnauthorizedBox < BaseObject - # Hide `"a"` - def self.authorized?(value, context) - super && value != "a" + field :value, String, null: false, method: :object end - field :value, String, null: false, method: :object - end - - module UnauthorizedInterface - include BaseInterface + module UnauthorizedInterface + include BaseInterface - def self.resolve_type(obj, ctx) - if obj.is_a?(String) - UnauthorizedCheckBox - else - raise "Unexpected value: #{obj.inspect}" + def self.resolve_type(obj, ctx) + if obj.is_a?(String) + UnauthorizedCheckBox + else + raise "Unexpected value: #{obj.inspect}" + end 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}" + 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 - 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: :object - end + field :value, String, null: false, method: :object + end - class IntegerObject < BaseObject - def self.authorized?(obj, ctx) - if !obj.is_a?(Integer) - raise "Unexpected IntegerObject: #{obj}" + 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 - is_allowed = !(ctx[:unauthorized_relay] || obj == ctx[:exclude_integer]) - Box.new(value: Box.new(value: is_allowed)) + field :value, Integer, null: false, method: :object end - field :value, Integer, null: false, method: :object - 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 } + class IntegerObjectEdge < GraphQL::Types::Relay::BaseEdge + node_type(IntegerObject) end - def replaced - false + class IntegerObjectConnection < GraphQL::Types::Relay::BaseConnection + edge_type(IntegerObjectEdge) end - end - class ReplacedObject < BaseObject - def self.authorized?(obj, ctx) - super && !ctx[:replace_me] - 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 - field :replaced, Boolean, null: false - end + def replaced + false + end + end - class LandscapeFeature < BaseEnum - value "MOUNTAIN" - value "STREAM", role: :unauthorized - value "FIELD", role: :inaccessible - value "TAR_PIT", role: :hidden - end + class ReplacedObject < BaseObject + def self.authorized?(obj, ctx) + super && !ctx[:replace_me] + end - class Query < BaseObject - field :hidden, Integer, null: false - field :unauthorized, Integer, null: true, method: :object - field :int2, Integer, null: true do - argument :int, Integer, required: false - argument :hidden, Integer, required: false - argument :inaccessible, Integer, required: false - argument :unauthorized, Integer, required: false + field :replaced, Boolean, null: false end - def int2(**args) - args[:unauthorized] || 1 + class LandscapeFeature < BaseEnum + value "MOUNTAIN" + value "STREAM", role: :unauthorized + value "FIELD", role: :inaccessible + value "TAR_PIT", role: :hidden end - field :landscape_feature, LandscapeFeature, null: false do - argument :string, String, required: false - argument :enum, LandscapeFeature, required: false - end + class Query < BaseObject + field :hidden, Integer, null: false + field :unauthorized, Integer, null: true, method: :object + field :int2, Integer, null: true do + argument :int, Integer, required: false + argument :hidden, Integer, required: false + argument :inaccessible, Integer, required: false + argument :unauthorized, Integer, required: false + end - def landscape_feature(string: nil, enum: nil) - string || enum - end + def int2(**args) + args[:unauthorized] || 1 + end - field :landscape_features, [LandscapeFeature], null: false do - argument :strings, [String], required: false - argument :enums, [LandscapeFeature], required: false - end + field :landscape_feature, LandscapeFeature, null: false do + argument :string, String, required: false + argument :enum, LandscapeFeature, required: false + end - def landscape_features(strings: [], enums: []) - strings + enums - end + def landscape_feature(string: nil, enum: nil) + string || enum + end - def empty_array; []; end - field :hidden_object, HiddenObject, null: false, method: :itself - field :hidden_interface, HiddenInterface, null: false, method: :itself - field :hidden_default_interface, HiddenDefaultInterface, null: false, method: :itself - field :hidden_connection, RelayObject.connection_type, null: :false, method: :empty_array - field :hidden_edge, RelayObject.edge_type, null: :false, method: :edge_object + field :landscape_features, [LandscapeFeature], null: false do + argument :strings, [String], required: false + argument :enums, [LandscapeFeature], required: false + end - field :inaccessible, Integer, null: false, method: :object_id - field :inaccessible_object, InaccessibleObject, null: false, method: :itself - field :inaccessible_interface, InaccessibleInterface, null: false, method: :itself - field :inaccessible_default_interface, InaccessibleDefaultInterface, null: false, method: :itself - field :inaccessible_connection, RelayObject.connection_type, null: :false, method: :empty_array - field :inaccessible_edge, RelayObject.edge_type, null: :false, method: :edge_object + def landscape_features(strings: [], enums: []) + strings + enums + end - field :unauthorized_object, UnauthorizedObject, null: true, method: :itself - field :unauthorized_connection, RelayObject.connection_type, null: false, method: :array_with_item - field :unauthorized_edge, RelayObject.edge_type, null: false, method: :edge_object + def empty_array; []; end + field :hidden_object, HiddenObject, null: false, method: :itself + field :hidden_interface, HiddenInterface, null: false, method: :itself + field :hidden_default_interface, HiddenDefaultInterface, null: false, method: :itself + field :hidden_connection, RelayObject.connection_type, null: :false, method: :empty_array + field :hidden_edge, RelayObject.edge_type, null: :false, method: :edge_object + + field :inaccessible, Integer, null: false, method: :object_id + field :inaccessible_object, InaccessibleObject, null: false, method: :itself + field :inaccessible_interface, InaccessibleInterface, null: false, method: :itself + field :inaccessible_default_interface, InaccessibleDefaultInterface, null: false, method: :itself + field :inaccessible_connection, RelayObject.connection_type, null: :false, method: :empty_array + field :inaccessible_edge, RelayObject.edge_type, null: :false, method: :edge_object + + field :unauthorized_object, UnauthorizedObject, null: true, method: :itself + field :unauthorized_connection, RelayObject.connection_type, null: false, method: :array_with_item + field :unauthorized_edge, RelayObject.edge_type, null: false, method: :edge_object + + def edge_object + OpenStruct.new(node: 100) + end - def edge_object - OpenStruct.new(node: 100) - end + def array_with_item + [1] + end - def array_with_item - [1] - end + field :unauthorized_lazy_box, UnauthorizedBox, null: true do + argument :value, String, required: true + 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], null: true + def unauthorized_list_items + [self, self] + end - field :unauthorized_lazy_box, UnauthorizedBox, null: true do - argument :value, String, required: true - 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], null: true - def unauthorized_list_items - [self, self] - end + field :unauthorized_lazy_check_box, UnauthorizedCheckBox, null: true, method: :unauthorized_lazy_box do + argument :value, String, required: true + end - field :unauthorized_lazy_check_box, UnauthorizedCheckBox, null: true, method: :unauthorized_lazy_box do - argument :value, String, required: true - end + field :unauthorized_interface, UnauthorizedInterface, null: true, method: :unauthorized_lazy_box do + argument :value, String, required: true + end - field :unauthorized_interface, UnauthorizedInterface, null: true, method: :unauthorized_lazy_box do - argument :value, String, required: true - end + field :unauthorized_lazy_list_interface, [UnauthorizedInterface, null: true], null: true - field :unauthorized_lazy_list_interface, [UnauthorizedInterface, null: true], null: true + def unauthorized_lazy_list_interface + ["z", Box.new(value: Box.new(value: "z2")), "a", Box.new(value: "a")] + end - def unauthorized_lazy_list_interface - ["z", Box.new(value: Box.new(value: "z2")), "a", Box.new(value: "a")] - end + field :integers, IntegerObjectConnection, null: false - field :integers, IntegerObjectConnection, null: false + def integers + [1,2,3] + end - def integers - [1,2,3] - end + field :lazy_integers, IntegerObjectConnection, null: false - field :lazy_integers, IntegerObjectConnection, null: false + def lazy_integers + Box.new(value: Box.new(value: [1,2,3])) + end - def lazy_integers - Box.new(value: Box.new(value: [1,2,3])) + field :replaced_object, ReplacedObject, null: false + def replaced_object + Replaceable.new + end end - field :replaced_object, ReplacedObject, null: false - def replaced_object - Replaceable.new + class DoHiddenStuff < GraphQL::Schema::RelayClassicMutation + def self.visible?(ctx) + super && (ctx[:hidden_mutation] ? false : true) + end end - end - class DoHiddenStuff < GraphQL::Schema::RelayClassicMutation - def self.visible?(ctx) - super && (ctx[:hidden_mutation] ? false : true) + class DoHiddenStuff2 < GraphQL::Schema::Mutation + def self.visible?(ctx) + super && !ctx[:hidden_mutation] + end end - end - class DoHiddenStuff2 < GraphQL::Schema::Mutation - def self.visible?(ctx) - super && !ctx[:hidden_mutation] + class DoInaccessibleStuff < GraphQL::Schema::RelayClassicMutation + def self.accessible?(ctx) + super && (ctx[:inaccessible_mutation] ? false : true) + end end - end - class DoInaccessibleStuff < GraphQL::Schema::RelayClassicMutation - def self.accessible?(ctx) - super && (ctx[:inaccessible_mutation] ? false : true) + class DoUnauthorizedStuff < GraphQL::Schema::RelayClassicMutation + def self.authorized?(obj, ctx) + super && (ctx[:unauthorized_mutation] ? false : true) + end end - end - class DoUnauthorizedStuff < GraphQL::Schema::RelayClassicMutation - def self.authorized?(obj, ctx) - super && (ctx[:unauthorized_mutation] ? false : true) + class Mutation < BaseObject + field :do_hidden_stuff, mutation: DoHiddenStuff + field :do_hidden_stuff2, mutation: DoHiddenStuff2 + field :do_inaccessible_stuff, mutation: DoInaccessibleStuff + field :do_unauthorized_stuff, mutation: DoUnauthorizedStuff + end + + class Schema < GraphQL::Schema + if TESTING_INTERPRETER + use GraphQL::Execution::Interpreter + end + query(Query) + mutation(Mutation) + + lazy_resolve(Box, :value) + + def self.unauthorized_object(err) + if err.object.respond_to?(:replacement) + err.object.replacement + else + raise GraphQL::ExecutionError, "Unauthorized #{err.type.graphql_name}: #{err.object}" + end + end + + # use GraphQL::Backtrace end end - class Mutation < BaseObject - field :do_hidden_stuff, mutation: DoHiddenStuff - field :do_hidden_stuff2, mutation: DoHiddenStuff2 - field :do_inaccessible_stuff, mutation: DoInaccessibleStuff - field :do_unauthorized_stuff, mutation: DoUnauthorizedStuff + def auth_execute(*args) + AuthTest::Schema.execute(*args) end - class Schema < GraphQL::Schema - if TESTING_INTERPRETER - use GraphQL::Execution::Interpreter + 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 - query(Query) - mutation(Mutation) - lazy_resolve(Box, :value) + 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"] } - def self.unauthorized_object(err) - if err.object.respond_to?(:replacement) - err.object.replacement - else - raise GraphQL::ExecutionError, "Unauthorized #{err.type.graphql_name}: #{err.object}" + 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 - # use GraphQL::Backtrace - end - end - - def auth_execute(*args) - AuthTest::Schema.execute(*args) - 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 "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"] } - it "applies return type visibility to fields" do - error_queries = { - "hiddenObject" => "{ hiddenObject { __typename } }", - "hiddenInterface" => "{ hiddenInterface { __typename } }", - "hiddenDefaultInterface" => "{ hiddenDefaultInterface { __typename } }", - } + # `#resolve` isn't implemented, so this errors out: + assert_raises NotImplementedError do + auth_execute(query) + end - 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"] } + 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_res = auth_execute(q) - # Both fields exist; the interface resolves to the object type, though - assert_equal "HiddenObject", visible_res["data"][name]["__typename"] + 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 - 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"] } + 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 NotImplementedError do - auth_execute(query) + # `#resolve` isn't implemented, so this errors out: + assert_raises NotImplementedError do + auth_execute(query) + end end - introspection_q = <<-GRAPHQL + it "uses the base type for edges and connections" do + query = <<-GRAPHQL { - t1: __type(name: "DoHiddenStuffInput") { name } - t2: __type(name: "DoHiddenStuffPayload") { name } + hiddenConnection { __typename } + hiddenEdge { __typename } } - 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 + GRAPHQL - 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"] } + hidden_res = auth_execute(query, context: { hidden_relay: true }) + assert_equal 2, hidden_res["errors"].size - # `#resolve` isn't implemented, so this errors out: - assert_raises NotImplementedError do - auth_execute(query) + visible_res = auth_execute(query) + assert_equal "RelayObjectConnection", visible_res["data"]["hiddenConnection"]["__typename"] + assert_equal "RelayObjectEdge", visible_res["data"]["hiddenEdge"]["__typename"] 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. 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. 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 + 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 "TAR_PIT", success_res["data"]["landscapeFeature"] - assert_equal ["STREAM", "TAR_PIT"], success_res["data"]["landscapeFeatures"] - end + assert_equal ["Argument 'enum' on Field 'landscapeFeature' has an invalid value. Expected type 'LandscapeFeature'."], hidden_res_1["errors"].map { |e| e["message"] } - it "refuses to resolve to hidden enum values" do - assert_raises(GraphQL::EnumType::UnresolvedValueError) do - auth_execute <<-GRAPHQL, context: { hide: true } + hidden_res_2 = auth_execute <<-GRAPHQL, context: { hide: true } { - landscapeFeature(string: "TAR_PIT") + landscapeFeatures(enums: [STREAM, TAR_PIT]) } GRAPHQL - end - assert_raises(GraphQL::EnumType::UnresolvedValueError) do - auth_execute <<-GRAPHQL, context: { hide: true } + assert_equal ["Argument 'enums' on Field 'landscapeFeatures' has an invalid value. Expected type '[LandscapeFeature!]'."], hidden_res_2["errors"].map { |e| e["message"] } + + success_res = auth_execute <<-GRAPHQL, context: { hide: false } { - landscapeFeatures(strings: ["STREAM", "TAR_PIT"]) + 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 - end - it "works in introspection" do - res = auth_execute <<-GRAPHQL, context: { hide: true, hidden_mutation: true } - { - query: __type(name: "Query") { - fields { - name - args { name } - } + it "refuses to resolve to hidden enum values" do + assert_raises(GraphQL::EnumType::UnresolvedValueError) do + auth_execute <<-GRAPHQL, context: { hide: true } + { + landscapeFeature(string: "TAR_PIT") } + GRAPHQL + end - 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", "inaccessible", "unauthorized"], int2_arg_names + assert_raises(GraphQL::EnumType::UnresolvedValueError) do + auth_execute <<-GRAPHQL, context: { hide: true } + { + landscapeFeatures(strings: ["STREAM", "TAR_PIT"]) + } + GRAPHQL + end + end - assert_nil res["data"]["hiddenObject"] - assert_nil res["data"]["hiddenInterface"] + it "works in introspection" do + res = auth_execute <<-GRAPHQL, context: { hide: true, hidden_mutation: true } + { + query: __type(name: "Query") { + fields { + name + args { name } + } + } - visible_landscape_features = res["data"]["landscapeFeatures"]["enumValues"].map { |v| v["name"] } - assert_equal ["MOUNTAIN", "STREAM", "FIELD"], visible_landscape_features + 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", "inaccessible", "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 end - end - describe "applying the accessible? method" do - it "works with fields and arguments" do - queries = { - "{ inaccessible }" => ["Some fields in this query are not accessible: inaccessible"], - "{ int2(inaccessible: 1) }" => ["Some fields in this query are not accessible: int2"], - } + describe "applying the accessible? method" do + it "works with fields and arguments" do + queries = { + "{ inaccessible }" => ["Some fields in this query are not accessible: inaccessible"], + "{ int2(inaccessible: 1) }" => ["Some fields in this query are not accessible: int2"], + } - queries.each do |query_str, errors| - res = auth_execute(query_str, context: { hide: true }) - assert_equal errors, res.fetch("errors").map { |e| e["message"] } + queries.each do |query_str, errors| + res = auth_execute(query_str, context: { hide: true }) + assert_equal errors, res.fetch("errors").map { |e| e["message"] } - res = auth_execute(query_str, context: { hide: false }) - refute res.key?("errors") + res = auth_execute(query_str, context: { hide: false }) + refute res.key?("errors") + end end - end - it "works with return types" do - queries = { - "{ inaccessibleObject { __typename } }" => ["Some fields in this query are not accessible: inaccessibleObject"], - "{ inaccessibleInterface { __typename } }" => ["Some fields in this query are not accessible: inaccessibleInterface"], - "{ inaccessibleDefaultInterface { __typename } }" => ["Some fields in this query are not accessible: inaccessibleDefaultInterface"], - } + it "works with return types" do + queries = { + "{ inaccessibleObject { __typename } }" => ["Some fields in this query are not accessible: inaccessibleObject"], + "{ inaccessibleInterface { __typename } }" => ["Some fields in this query are not accessible: inaccessibleInterface"], + "{ inaccessibleDefaultInterface { __typename } }" => ["Some fields in this query are not accessible: inaccessibleDefaultInterface"], + } - queries.each do |query_str, errors| - res = auth_execute(query_str, context: { hide: true }) - assert_equal errors, res["errors"].map { |e| e["message"] } + queries.each do |query_str, errors| + res = auth_execute(query_str, context: { hide: true }) + assert_equal errors, res["errors"].map { |e| e["message"] } - res = auth_execute(query_str, context: { hide: false }) - refute res.key?("errors") + res = auth_execute(query_str, context: { hide: false }) + refute res.key?("errors") + end end - end - it "works with mutations" do - query = "mutation { doInaccessibleStuff(input: {}) { __typename } }" - res = auth_execute(query, context: { inaccessible_mutation: true }) - assert_equal ["Some fields in this query are not accessible: doInaccessibleStuff"], res["errors"].map { |e| e["message"] } + it "works with mutations" do + query = "mutation { doInaccessibleStuff(input: {}) { __typename } }" + res = auth_execute(query, context: { inaccessible_mutation: true }) + assert_equal ["Some fields in this query are not accessible: doInaccessibleStuff"], res["errors"].map { |e| e["message"] } - assert_raises NotImplementedError do - auth_execute(query) + assert_raises NotImplementedError do + auth_execute(query) + end end - end - it "works with edges and connections" do - query = <<-GRAPHQL - { - inaccessibleConnection { __typename } - inaccessibleEdge { __typename } - } - GRAPHQL + it "works with edges and connections" do + query = <<-GRAPHQL + { + inaccessibleConnection { __typename } + inaccessibleEdge { __typename } + } + GRAPHQL - inaccessible_res = auth_execute(query, context: { inaccessible_relay: true }) - assert_equal ["Some fields in this query are not accessible: inaccessibleConnection, inaccessibleEdge"], inaccessible_res["errors"].map { |e| e["message"] } + inaccessible_res = auth_execute(query, context: { inaccessible_relay: true }) + assert_equal ["Some fields in this query are not accessible: inaccessibleConnection, inaccessibleEdge"], inaccessible_res["errors"].map { |e| e["message"] } - accessible_res = auth_execute(query) - refute accessible_res.key?("errors") + accessible_res = auth_execute(query) + refute accessible_res.key?("errors") + end 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 + 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 NotImplementedError do - auth_execute(query) + 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 NotImplementedError do + auth_execute(query) + 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 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 "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 { + it "works with edges and connections" do + query = <<-GRAPHQL + { + unauthorizedConnection { __typename - node { + edges { + __typename + node { + __typename + } + } + nodes { __typename } } - nodes { + unauthorizedEdge { __typename + node { + __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 = TESTING_INTERPRETER ? [nil] : 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"], + TESTING_INTERPRETER ? ["unauthorizedConnection", "nodes", 0] : ["unauthorizedConnection", "nodes"], + ["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, 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 = TESTING_INTERPRETER ? [nil] : 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"], - TESTING_INTERPRETER ? ["unauthorizedConnection", "nodes", 0] : ["unauthorizedConnection", "nodes"], - ["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 + GRAPHQL - 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 + 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 + it "authorizes items in a list" do + query = <<-GRAPHQL + { + unauthorizedListItems { __typename } + } + GRAPHQL - unauthorized_res = auth_execute(query, context: { hide: true }) + 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 + 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 "syncs lazy objects from authorized? checks" do + query = <<-GRAPHQL + { + a: unauthorizedLazyCheckBox(value: "a") { value } + b: unauthorizedLazyCheckBox(value: "b") { value } + } + GRAPHQL - 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 + 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 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 "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 "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 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 "works with lazy values / interfaces" do - query = <<-GRAPHQL - query($value: String!){ - unauthorizedInterface(value: $value) { - ... on UnauthorizedCheckBox { - value + 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 + GRAPHQL - res = auth_execute(query, variables: { value: "a"}) - assert_nil res["data"]["unauthorizedInterface"] + 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 + 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 + it "works with lazy values / lists of interfaces" do + query = <<-GRAPHQL + { + unauthorizedLazyListInterface { + ... on UnauthorizedCheckBox { + value + } } } - } - GRAPHQL + 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 + 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 - 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"] + 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"] + res = auth_execute(query, context: { replace_me: false }) + assert_equal false, res["data"]["replacedObject"]["replaced"] + end end end end From 08341fd83ba88b7d55b616faddbd60793e5d758a Mon Sep 17 00:00:00 2001 From: Marc-Andre Giroux Date: Wed, 24 Oct 2018 12:32:17 -0400 Subject: [PATCH 3/3] dont analyze if there is no selected operation --- lib/graphql/analysis/ast/visitor.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/graphql/analysis/ast/visitor.rb b/lib/graphql/analysis/ast/visitor.rb index 75121329e2..5907ec5963 100644 --- a/lib/graphql/analysis/ast/visitor.rb +++ b/lib/graphql/analysis/ast/visitor.rb @@ -32,6 +32,11 @@ def initialize(query:, analyzers:) # @return [Array] Types whose scope we've entered attr_reader :object_types + def visit + return if @document.nil? + super + end + # Visit Helpers # @return [GraphQL::Query::Arguments] Arguments for this node, merging default values, literal values and query variables