# frozen_string_literal: true require "spec_helper" describe GraphQL::Analysis::AST do class AstTypeCollector < GraphQL::Analysis::AST::Analyzer def initialize(query) super @types = [] end def on_enter_operation_definition(node, parent, visitor) @types << visitor.type_definition end def on_enter_field(memo, node, visitor) @types << visitor.field_definition.type.unwrap end def result @types end end class AstNodeCounter < GraphQL::Analysis::AST::Analyzer def initialize(query) super @nodes = Hash.new { |h,k| h[k] = 0 } end def on_enter_abstract_node(node, parent, _visitor) @nodes[node.class] += 1 end def result @nodes end end class AstConditionalAnalyzer < GraphQL::Analysis::AST::Analyzer def initialize(query) super @i_have_been_called = false end def analyze? !!query.context[:analyze] end def on_operation_definition(node, parent, visitor) @i_have_been_called = true end def result @i_have_been_called end end class AstErrorAnalyzer < GraphQL::Analysis::AST::Analyzer def result GraphQL::AnalysisError.new("An Error!") end end class AstPreviousField < GraphQL::Analysis::AST::Analyzer def on_enter_field(node, parent, visitor) @previous_field = visitor.previous_field_definition end def result @previous_field end end describe "using the AST analysis engine" do let(:schema) do query_type = Class.new(GraphQL::Schema::Object) do graphql_name 'Query' field :foobar, Integer, null: false def foobar 1337 end end Class.new(GraphQL::Schema) do query query_type use GraphQL::Analysis::AST query_analyzer AstErrorAnalyzer use GraphQL::Execution::Interpreter end end let(:query_string) {%| query { foobar } |} let(:query) { GraphQL::Query.new(schema, query_string, variables: {}) } it "runs the AST analyzers correctly" do res = query.result refute res.key?("data") assert_equal ["An Error!"], res["errors"].map { |e| e["message"] } end it "skips rewrite" do # Try running the query: query.result # But the validation step doesn't build an irep_node tree assert_nil query.irep_selection end describe "when validate: false" do let(:query) { GraphQL::Query.new(schema, query_string, validate: false) } it "Skips rewrite" do # Try running the query: query.result # But the validation step doesn't build an irep_node tree assert_nil query.irep_selection end end end describe ".analyze_query" do let(:analyzers) { [AstTypeCollector, AstNodeCounter] } let(:reduce_result) { GraphQL::Analysis::AST.analyze_query(query, analyzers) } let(:variables) { {} } let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables) } let(:query_string) {%| { cheese(id: 1) { id flavor } } |} describe "without a selected operation" do let(:query_string) {%| # A comment # And nothing else # Should not break |} it "bails early when there is no selected operation to be executed" do assert_equal 2, reduce_result.size end end describe "conditional analysis" do let(:analyzers) { [AstTypeCollector, AstConditionalAnalyzer] } describe "when analyze? returns false" do let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables, context: { analyze: false }) } it "does not run the analyzer" do # Only type_collector ran assert_equal 1, reduce_result.size end end describe "when analyze? returns true" do let(:query) { GraphQL::Query.new(Dummy::Schema, query_string, variables: variables, context: { analyze: true }) } it "it runs the analyzer" do # Both analyzers ran assert_equal 2, reduce_result.size end end describe "Visitor#previous_field_definition" do let(:analyzers) { [AstPreviousField] } let(:query) { GraphQL::Query.new(Dummy::Schema, "{ __schema { types { name } } }") } it "it runs the analyzer" do prev_field = reduce_result.first assert_equal "__Schema.types", prev_field.metadata[:type_class].path end end end it "calls the defined analyzers" do collected_types, node_counts = reduce_result expected_visited_types = [ Dummy::DairyAppQuery.graphql_definition, Dummy::Cheese.graphql_definition, GraphQL::INT_TYPE, GraphQL::STRING_TYPE ] assert_equal expected_visited_types, collected_types expected_node_counts = { GraphQL::Language::Nodes::OperationDefinition => 1, GraphQL::Language::Nodes::Field => 3, GraphQL::Language::Nodes::Argument => 1 } assert_equal expected_node_counts, node_counts end describe "tracing" do let(:query_string) { "{ t: __typename }"} it "emits traces" do traces = TestTracing.with_trace do ctx = { tracers: [TestTracing] } Dummy::Schema.execute(query_string, context: ctx) end # The query_trace is on the list _first_ because it finished first _lex, _parse, _validate, query_trace, multiplex_trace, *_rest = traces assert_equal "analyze_multiplex", multiplex_trace[:key] assert_instance_of GraphQL::Execution::Multiplex, multiplex_trace[:multiplex] assert_equal "analyze_query", query_trace[:key] assert_instance_of GraphQL::Query, query_trace[:query] end end class AstConnectionCounter < GraphQL::Analysis::AST::Analyzer def initialize(query) super @fields = 0 @connections = 0 end def on_enter_field(node, parent, visitor) if visitor.field_definition.connection? @connections += 1 else @fields += 1 end end def result { fields: @fields, connections: @connections } end end describe "when processing fields" do let(:analyzers) { [AstConnectionCounter] } let(:reduce_result) { GraphQL::Analysis::AST.analyze_query(query, analyzers) } let(:query) { GraphQL::Query.new(StarWars::Schema, query_string, variables: variables) } let(:query_string) {%| query getBases { empire { basesByName(first: 30) { edges { cursor } } bases(first: 30) { edges { cursor } } } } |} it "knows which fields are connections" do connection_counts = reduce_result.first expected_connection_counts = { :fields => 5, :connections => 2 } assert_equal expected_connection_counts, connection_counts end end end end