diff --git a/lib/absinthe.ex b/lib/absinthe.ex index 5ed484cdc2..d422e19e12 100644 --- a/lib/absinthe.ex +++ b/lib/absinthe.ex @@ -70,6 +70,8 @@ defmodule Absinthe do executing an operation. * `:max_complexity` -> An integer (or `:infinity`) for the maximum allowed complexity for the operation being executed. + # `:maximum_number_of_suggestions` -> An integer for the maximum number of suggestions + to be used on error messages. 0 can be passed to disable suggestions and avoid schema instrospection. ## Examples @@ -95,7 +97,8 @@ defmodule Absinthe do analyze_complexity: boolean, variables: %{optional(String.t()) => any()}, max_complexity: non_neg_integer | :infinity, - pipeline_modifier: pipeline_modifier_fun() + pipeline_modifier: pipeline_modifier_fun(), + maximum_number_of_suggestions: non_neg_integer() | nil ] @type run_result :: {:ok, result_t} | {:error, String.t()} diff --git a/lib/absinthe/phase/document/validation/arguments_of_correct_type.ex b/lib/absinthe/phase/document/validation/arguments_of_correct_type.ex index f1dfd058f7..4f737fbe38 100644 --- a/lib/absinthe/phase/document/validation/arguments_of_correct_type.ex +++ b/lib/absinthe/phase/document/validation/arguments_of_correct_type.ex @@ -11,26 +11,30 @@ defmodule Absinthe.Phase.Document.Validation.ArgumentsOfCorrectType do Run this validation. """ @spec run(Blueprint.t(), Keyword.t()) :: Phase.result_t() - def run(input, _options \\ []) do - result = Blueprint.prewalk(input, &handle_node(&1, input.schema)) + def run(input, options \\ []) do + result = Blueprint.prewalk(input, &handle_node(&1, input.schema, options)) {:ok, result} end # Check arguments, objects, fields, and lists - @spec handle_node(Blueprint.node_t(), Schema.t()) :: Blueprint.node_t() + @spec handle_node(Blueprint.node_t(), Schema.t(), Absinthe.run_opts()) :: Blueprint.node_t() # handled by Phase.Document.Validation.KnownArgumentNames - defp handle_node(%Blueprint.Input.Argument{schema_node: nil} = node, _schema) do + defp handle_node(%Blueprint.Input.Argument{schema_node: nil} = node, _schema, _options) do {:halt, node} end # handled by Phase.Document.Validation.ProvidedNonNullArguments - defp handle_node(%Blueprint.Input.Argument{input_value: %{normalized: nil}} = node, _schema) do + defp handle_node( + %Blueprint.Input.Argument{input_value: %{normalized: nil}} = node, + _schema, + _options + ) do {:halt, node} end - defp handle_node(%Blueprint.Input.Argument{flags: %{invalid: _}} = node, schema) do - descendant_errors = collect_child_errors(node.input_value, schema) + defp handle_node(%Blueprint.Input.Argument{flags: %{invalid: _}} = node, schema, options) do + descendant_errors = collect_child_errors(node.input_value, schema, options) message = error_message( @@ -46,17 +50,17 @@ defmodule Absinthe.Phase.Document.Validation.ArgumentsOfCorrectType do {:halt, node} end - defp handle_node(node, _) do + defp handle_node(node, _, _options) do node end - defp collect_child_errors(%Blueprint.Input.List{} = node, schema) do + defp collect_child_errors(%Blueprint.Input.List{} = node, schema, options) do node.items |> Enum.map(& &1.normalized) |> Enum.with_index() |> Enum.flat_map(fn {%{schema_node: nil} = child, _} -> - collect_child_errors(child, schema) + collect_child_errors(child, schema, options) {%{flags: %{invalid: invalid_flag}} = child, idx} -> child_type_name = @@ -73,15 +77,15 @@ defmodule Absinthe.Phase.Document.Validation.ArgumentsOfCorrectType do child_inspected_value, custom_error(invalid_flag) ) - | collect_child_errors(child, schema) + | collect_child_errors(child, schema, options) ] {child, _} -> - collect_child_errors(child, schema) + collect_child_errors(child, schema, options) end) end - defp collect_child_errors(%Blueprint.Input.Object{} = node, schema) do + defp collect_child_errors(%Blueprint.Input.Object{} = node, schema, options) do node.fields |> Enum.flat_map(fn %{flags: %{invalid: _}, schema_node: nil} = child -> @@ -93,7 +97,7 @@ defmodule Absinthe.Phase.Document.Validation.ArgumentsOfCorrectType do _ -> suggested_field_names(node.schema_node, child.name) end - [unknown_field_error_message(child.name, field_suggestions)] + [unknown_field_error_message(child.name, field_suggestions, options)] %{flags: %{invalid: invalid_flag}} = child -> child_type_name = @@ -104,7 +108,7 @@ defmodule Absinthe.Phase.Document.Validation.ArgumentsOfCorrectType do case child.schema_node do %Type.Scalar{} -> [] %Type.Enum{} -> [] - _ -> collect_child_errors(child.input_value, schema) + _ -> collect_child_errors(child.input_value, schema, options) end child_inspected_value = Blueprint.Input.inspect(child.input_value) @@ -120,22 +124,23 @@ defmodule Absinthe.Phase.Document.Validation.ArgumentsOfCorrectType do ] child -> - collect_child_errors(child.input_value.normalized, schema) + collect_child_errors(child.input_value.normalized, schema, options) end) end defp collect_child_errors( %Blueprint.Input.Value{normalized: %{flags: %{invalid: {_, reason}}} = norm}, - schema + schema, + options ) do - [reason | collect_child_errors(norm, schema)] + [reason | collect_child_errors(norm, schema, options)] end - defp collect_child_errors(%Blueprint.Input.Value{normalized: norm}, schema) do - collect_child_errors(norm, schema) + defp collect_child_errors(%Blueprint.Input.Value{normalized: norm}, schema, options) do + collect_child_errors(norm, schema, options) end - defp collect_child_errors(_node, _) do + defp collect_child_errors(_node, _, _options) do [] end @@ -179,15 +184,15 @@ defmodule Absinthe.Phase.Document.Validation.ArgumentsOfCorrectType do expected_type_error_message(expected_type_name, inspected_value, custom_error) end - def unknown_field_error_message(field_name, suggestions \\ []) + def unknown_field_error_message(field_name, suggestions \\ [], options \\ []) - def unknown_field_error_message(field_name, []) do + def unknown_field_error_message(field_name, [], _options) do ~s(In field "#{field_name}": Unknown field.) end - def unknown_field_error_message(field_name, suggestions) do + def unknown_field_error_message(field_name, suggestions, options) do ~s(In field "#{field_name}": Unknown field.) <> - Utils.MessageSuggestions.suggest_message(suggestions) + Utils.MessageSuggestions.suggest_message(suggestions, options) end defp expected_type_error_message(expected_type_name, inspected_value, nil) do diff --git a/lib/absinthe/phase/document/validation/fields_on_correct_type.ex b/lib/absinthe/phase/document/validation/fields_on_correct_type.ex index 58911b58d6..1ebaee5b31 100644 --- a/lib/absinthe/phase/document/validation/fields_on_correct_type.ex +++ b/lib/absinthe/phase/document/validation/fields_on_correct_type.ex @@ -11,13 +11,13 @@ defmodule Absinthe.Phase.Document.Validation.FieldsOnCorrectType do Run the validation. """ @spec run(Blueprint.t(), Keyword.t()) :: Phase.result_t() - def run(input, _options \\ []) do - result = Blueprint.prewalk(input, &handle_node(&1, input)) + def run(input, options \\ []) do + result = Blueprint.prewalk(input, &handle_node(&1, input, options)) {:ok, result} end - @spec handle_node(Blueprint.node_t(), Schema.t()) :: Blueprint.node_t() - defp handle_node(%Blueprint.Document.Operation{schema_node: nil} = node, _) do + @spec handle_node(Blueprint.node_t(), Schema.t(), Absinthe.run_opts()) :: Blueprint.node_t() + defp handle_node(%Blueprint.Document.Operation{schema_node: nil} = node, _, _options) do error = %Phase.Error{ phase: __MODULE__, message: "Operation \"#{node.type}\" not supported", @@ -31,7 +31,8 @@ defmodule Absinthe.Phase.Document.Validation.FieldsOnCorrectType do defp handle_node( %{selections: selections, schema_node: parent_schema_node} = node, - %{schema: schema} = input + %{schema: schema} = input, + options ) when not is_nil(parent_schema_node) do possible_parent_types = possible_types(parent_schema_node, schema) @@ -41,16 +42,18 @@ defmodule Absinthe.Phase.Document.Validation.FieldsOnCorrectType do %Blueprint.Document.Field{schema_node: nil} = field -> type = named_type(parent_schema_node, schema) - field - |> flag_invalid(:unknown_field) - |> put_error( + error = error( field, type.name, suggested_type_names(field.name, type, input), - suggested_field_names(field.name, type, input) + suggested_field_names(field.name, type, input), + options ) - ) + + field + |> flag_invalid(:unknown_field) + |> put_error(error) %Blueprint.Document.Fragment.Spread{errors: []} = spread -> fragment = Enum.find(input.fragments, &(&1.name == spread.name)) @@ -59,7 +62,7 @@ defmodule Absinthe.Phase.Document.Validation.FieldsOnCorrectType do if Enum.any?(possible_child_types, &(&1 in possible_parent_types)) do spread else - spread_error(spread, possible_parent_types, possible_child_types, schema) + spread_error(spread, possible_parent_types, possible_child_types, schema, options) end %Blueprint.Document.Fragment.Inline{} = fragment -> @@ -68,7 +71,7 @@ defmodule Absinthe.Phase.Document.Validation.FieldsOnCorrectType do if Enum.any?(possible_child_types, &(&1 in possible_parent_types)) do fragment else - spread_error(fragment, possible_parent_types, possible_child_types, schema) + spread_error(fragment, possible_parent_types, possible_child_types, schema, options) end other -> @@ -78,7 +81,7 @@ defmodule Absinthe.Phase.Document.Validation.FieldsOnCorrectType do %{node | selections: selections} end - defp handle_node(node, _) do + defp handle_node(node, _, _options) do node end @@ -88,7 +91,7 @@ defmodule Absinthe.Phase.Document.Validation.FieldsOnCorrectType do end end - defp spread_error(spread, parent_types_idents, child_types_idents, schema) do + defp spread_error(spread, parent_types_idents, child_types_idents, schema, _options) do parent_types = idents_to_names(parent_types_idents, schema) child_types = idents_to_names(child_types_idents, schema) @@ -142,12 +145,21 @@ defmodule Absinthe.Phase.Document.Validation.FieldsOnCorrectType do end # Generate the error for a field - @spec error(Blueprint.node_t(), String.t(), [String.t()], [String.t()]) :: Phase.Error.t() - defp error(field_node, parent_type_name, type_suggestions, field_suggestions) do + @spec error(Blueprint.node_t(), String.t(), [String.t()], [String.t()], Absinthe.run_opts()) :: + Phase.Error.t() + defp error(field_node, parent_type_name, type_suggestions, field_suggestions, options) do + message = + error_message( + field_node.name, + parent_type_name, + type_suggestions, + field_suggestions, + options + ) + %Phase.Error{ phase: __MODULE__, - message: - error_message(field_node.name, parent_type_name, type_suggestions, field_suggestions), + message: message, locations: [field_node.source_location] } end @@ -155,25 +167,32 @@ defmodule Absinthe.Phase.Document.Validation.FieldsOnCorrectType do @doc """ Generate an error for a field """ - @spec error_message(String.t(), String.t(), [String.t()], [String.t()]) :: String.t() - def error_message(field_name, type_name, type_suggestions \\ [], field_suggestions \\ []) - - def error_message(field_name, type_name, [], []) do + @spec error_message(String.t(), String.t(), [String.t()], [String.t()], Absinthe.run_opts()) :: + String.t() + def error_message( + field_name, + type_name, + type_suggestions \\ [], + field_suggestions \\ [], + options \\ [] + ) + + def error_message(field_name, type_name, [], [], _options) do ~s(Cannot query field "#{field_name}" on type "#{type_name}".) end - def error_message(field_name, type_name, [], field_suggestions) do - error_message(field_name, type_name) <> - Utils.MessageSuggestions.suggest_message(field_suggestions) + def error_message(field_name, type_name, [], field_suggestions, options) do + error_message(field_name, type_name, [], [], options) <> + Utils.MessageSuggestions.suggest_message(field_suggestions, options) end - def error_message(field_name, type_name, type_suggestions, []) do - error_message(field_name, type_name) <> - Utils.MessageSuggestions.suggest_fragment_message(type_suggestions) + def error_message(field_name, type_name, type_suggestions, [], options) do + error_message(field_name, type_name, [], [], options) <> + Utils.MessageSuggestions.suggest_fragment_message(type_suggestions, options) end - def error_message(field_name, type_name, type_suggestions, _) do - error_message(field_name, type_name, type_suggestions) + def error_message(field_name, type_name, type_suggestions, _, options) do + error_message(field_name, type_name, type_suggestions, [], options) end defp suggested_type_names(external_field_name, type, blueprint) do @@ -214,7 +233,7 @@ defmodule Absinthe.Phase.Document.Validation.FieldsOnCorrectType do defp find_possible_interfaces(field_name, possible_types, schema) do possible_types - |> types_to_interface_idents + |> types_to_interface_idents() |> Enum.uniq() |> sort_by_implementation_count(possible_types) |> Enum.map(&Schema.lookup_type(schema, &1)) diff --git a/lib/absinthe/phase/document/validation/scalar_leafs.ex b/lib/absinthe/phase/document/validation/scalar_leafs.ex index 07d59db28a..10db260b8b 100644 --- a/lib/absinthe/phase/document/validation/scalar_leafs.ex +++ b/lib/absinthe/phase/document/validation/scalar_leafs.ex @@ -35,7 +35,7 @@ defmodule Absinthe.Phase.Document.Validation.ScalarLeafs do # } # ``` - alias Absinthe.{Blueprint, Phase, Type} + alias Absinthe.{Blueprint, Phase, Phase.Document.Validation.Utils, Type} use Absinthe.Phase @@ -43,19 +43,19 @@ defmodule Absinthe.Phase.Document.Validation.ScalarLeafs do Run the validation. """ @spec run(Blueprint.t(), Keyword.t()) :: Phase.result_t() - def run(input, _options \\ []) do - result = Blueprint.prewalk(input, &handle_node(&1, input.schema)) + def run(input, options \\ []) do + result = Blueprint.prewalk(input, &handle_node(&1, input.schema, options)) {:ok, result} end - defp handle_node(%{schema_node: nil} = node, _schema), do: {:halt, node} + defp handle_node(%{schema_node: nil} = node, _schema, _options), do: {:halt, node} - defp handle_node(%Blueprint.Document.Field{schema_node: schema_node} = node, schema) do + defp handle_node(%Blueprint.Document.Field{schema_node: schema_node} = node, schema, options) do type = Type.expand(schema_node.type, schema) - process(node, Type.unwrap(type), type) + process(node, Type.unwrap(type), type, options) end - defp handle_node(node, _) do + defp handle_node(node, _, _options) do node end @@ -65,29 +65,30 @@ defmodule Absinthe.Phase.Document.Validation.ScalarLeafs do Type.Interface ] - defp process(%{selections: []} = node, %unwrapped{}, type) when unwrapped in @has_subfields do - bad_node(node, type, :missing_subfields) + defp process(%{selections: []} = node, %unwrapped{}, type, options) + when unwrapped in @has_subfields do + bad_node(node, type, :missing_subfields, options) end - defp process(%{selections: s} = node, %unwrapped{}, type) + defp process(%{selections: s} = node, %unwrapped{}, type, options) when s != [] and unwrapped not in @has_subfields do - bad_node(node, type, :bad_subfields) + bad_node(node, type, :bad_subfields, options) end - defp process(node, _, _) do + defp process(node, _, _, _options) do node end - defp bad_node(node, type, :bad_subfields = flag) do + defp bad_node(node, type, :bad_subfields = flag, _options) do node |> flag_invalid(flag) |> put_error(error(node, no_subselection_allowed_message(node.name, Type.name(type)))) end - defp bad_node(node, type, :missing_subfields = flag) do + defp bad_node(node, type, :missing_subfields = flag, options) do node |> flag_invalid(flag) - |> put_error(error(node, required_subselection_message(node.name, Type.name(type)))) + |> put_error(error(node, required_subselection_message(node.name, Type.name(type), options))) end # Generate the error @@ -111,8 +112,11 @@ defmodule Absinthe.Phase.Document.Validation.ScalarLeafs do @doc """ Generate the error message for a missing field subselection. """ - @spec required_subselection_message(String.t(), String.t()) :: String.t() - def required_subselection_message(field_name, type_name) do - ~s(Field "#{field_name}" of type "#{type_name}" must have a selection of subfields. Did you mean "#{field_name} { ... }"?) + @spec required_subselection_message(String.t(), String.t(), Absinthe.run_opts()) :: String.t() + def required_subselection_message(field_name, type_name, options) do + suggestions = ["#{field_name} { ... }"] + + ~s(Field "#{field_name}" of type "#{type_name}" must have a selection of subfields.) <> + Utils.MessageSuggestions.suggest_message(suggestions, options) end end diff --git a/lib/absinthe/phase/document/validation/utils/message_suggestions.ex b/lib/absinthe/phase/document/validation/utils/message_suggestions.ex index a605096b40..dc06a7db80 100644 --- a/lib/absinthe/phase/document/validation/utils/message_suggestions.ex +++ b/lib/absinthe/phase/document/validation/utils/message_suggestions.ex @@ -1,17 +1,34 @@ defmodule Absinthe.Phase.Document.Validation.Utils.MessageSuggestions do @moduledoc false - @suggest 5 + @default_maximum_number_of_suggestions 5 @doc """ Generate an suggestions message for a incorrect field """ - def suggest_message(suggestions) do - " Did you mean " <> to_quoted_or_list(suggestions |> Enum.take(@suggest)) <> "?" + @spec suggest_message([String.t()], Absinthe.run_opts()) :: String.t() + def suggest_message(suggestions, options) do + maximum_number_of_suggestions = maximum_number_of_suggestions(options) + + if maximum_number_of_suggestions == 0 do + "" + else + suggestions = Enum.take(suggestions, maximum_number_of_suggestions) + " Did you mean " <> to_quoted_or_list(suggestions) <> "?" + end end - def suggest_fragment_message(suggestions) do - " Did you mean to use an inline fragment on " <> - to_quoted_or_list(suggestions |> Enum.take(@suggest)) <> "?" + @spec suggest_fragment_message([String.t()], Absinthe.run_opts()) :: String.t() + def suggest_fragment_message(suggestions, options) do + maximum_number_of_suggestions = maximum_number_of_suggestions(options) + + if maximum_number_of_suggestions == 0 do + "" + else + suggestions = Enum.take(suggestions, maximum_number_of_suggestions) + + " Did you mean to use an inline fragment on " <> + to_quoted_or_list(suggestions) <> "?" + end end defp to_quoted_or_list([a]), do: ~s("#{a}") @@ -30,4 +47,9 @@ defmodule Absinthe.Phase.Document.Validation.Utils.MessageSuggestions do rest |> to_longer_quoted_or_list(acc <> ~s(, "#{word}")) end + + @spec maximum_number_of_suggestions(Absinthe.run_opts()) :: non_neg_integer() + defp maximum_number_of_suggestions(options) do + Keyword.get(options, :maximum_number_of_suggestions) || @default_maximum_number_of_suggestions + end end diff --git a/lib/absinthe/phase/validation/known_type_names.ex b/lib/absinthe/phase/validation/known_type_names.ex index bd46972826..86e8ff7c02 100644 --- a/lib/absinthe/phase/validation/known_type_names.ex +++ b/lib/absinthe/phase/validation/known_type_names.ex @@ -20,20 +20,22 @@ defmodule Absinthe.Phase.Validation.KnownTypeNames do Run the validation. """ @spec run(Blueprint.t(), Keyword.t()) :: Phase.result_t() - def run(input, _options \\ []) do - result = Blueprint.postwalk(input, &handle_node(&1, input.schema)) + def run(input, options \\ []) do + result = Blueprint.postwalk(input, &handle_node(&1, input.schema, options)) {:ok, result} end - defp handle_node(%{type_condition: type, schema_node: nil} = node, _) when not is_nil(type) do + defp handle_node(%{type_condition: type, schema_node: nil} = node, _, options) + when not is_nil(type) do name = Blueprint.TypeReference.unwrap(type).name + error = error(node, name, options) node |> flag_invalid(:bad_type_name) - |> put_error(error(node, name)) + |> put_error(error) end - defp handle_node(%Blueprint.Document.VariableDefinition{} = node, schema) do + defp handle_node(%Blueprint.Document.VariableDefinition{} = node, schema, options) do name = Blueprint.TypeReference.unwrap(node.type).name inner_schema_type = schema.__absinthe_lookup__(name) @@ -41,14 +43,15 @@ defmodule Absinthe.Phase.Validation.KnownTypeNames do node else suggestions = suggested_type_names(schema, name) + error = error(node, name, suggestions, options) node |> flag_invalid(:bad_type_name) - |> put_error(error(node, name, suggestions)) + |> put_error(error) end end - defp handle_node(node, _) do + defp handle_node(node, _, _options) do node end @@ -59,20 +62,20 @@ defmodule Absinthe.Phase.Validation.KnownTypeNames do |> Absinthe.Utils.Suggestion.sort_list(name) end - @spec error(Blueprint.node_t(), String.t()) :: Phase.Error.t() - defp error(node, name, suggestions \\ []) do + @spec error(Blueprint.node_t(), String.t(), Absinthe.run_opts()) :: Phase.Error.t() + defp error(node, name, suggestions \\ [], options) do %Phase.Error{ phase: __MODULE__, - message: message(name, suggestions), + message: message(name, suggestions, options), locations: [node.source_location] } end - defp message(name, []) do + defp message(name, [], _options) do ~s(Unknown type "#{name}".) end - defp message(name, suggestions) do - ~s(Unknown type "#{name}".) <> Utils.MessageSuggestions.suggest_message(suggestions) + defp message(name, suggestions, options) do + ~s(Unknown type "#{name}".) <> Utils.MessageSuggestions.suggest_message(suggestions, options) end end diff --git a/lib/absinthe/pipeline.ex b/lib/absinthe/pipeline.ex index 2c065957a8..96ff18af27 100644 --- a/lib/absinthe/pipeline.ex +++ b/lib/absinthe/pipeline.ex @@ -83,7 +83,7 @@ defmodule Absinthe.Pipeline do # Map to Schema {Phase.Schema, options}, # Ensure Types - Phase.Validation.KnownTypeNames, + {Phase.Validation.KnownTypeNames, options}, Phase.Document.Arguments.VariableTypesMatch, # Process Arguments Phase.Document.Arguments.CoerceEnums, @@ -95,14 +95,14 @@ defmodule Absinthe.Pipeline do # Validate Full Document Phase.Document.Validation.KnownDirectives, Phase.Document.Validation.RepeatableDirectives, - Phase.Document.Validation.ScalarLeafs, + {Phase.Document.Validation.ScalarLeafs, options}, Phase.Document.Validation.VariablesAreInputTypes, - Phase.Document.Validation.ArgumentsOfCorrectType, + {Phase.Document.Validation.ArgumentsOfCorrectType, options}, Phase.Document.Validation.KnownArgumentNames, Phase.Document.Validation.ProvidedNonNullArguments, Phase.Document.Validation.UniqueArgumentNames, Phase.Document.Validation.UniqueInputFieldNames, - Phase.Document.Validation.FieldsOnCorrectType, + {Phase.Document.Validation.FieldsOnCorrectType, options}, Phase.Document.Validation.OneOfDirective, Phase.Document.Validation.OnlyOneSubscription, # Check Validation diff --git a/test/absinthe/execution/arguments/input_object_test.exs b/test/absinthe/execution/arguments/input_object_test.exs index c194b43246..5ebb62ee10 100644 --- a/test/absinthe/execution/arguments/input_object_test.exs +++ b/test/absinthe/execution/arguments/input_object_test.exs @@ -172,6 +172,19 @@ defmodule Absinthe.Execution.Arguments.InputObjectTest do variables: %{"contact" => %{"email" => "bubba@joe.com", "default_with_stream" => "asdf"}} ) ) + + assert_error_message_lines( + [ + ~s(Argument "contact" has invalid value $contact.), + ~s(In field "default_with_stream": Unknown field.) + ], + run( + @graphql, + @schema, + variables: %{"contact" => %{"email" => "bubba@joe.com", "default_with_stream" => "asdf"}}, + maximum_number_of_suggestions: 0 + ) + ) end test "return field error with multiple suggestions" do @@ -191,6 +204,24 @@ defmodule Absinthe.Execution.Arguments.InputObjectTest do } ) ) + + assert_error_message_lines( + [ + ~s(Argument "contact" has invalid value $contact.), + ~s(In field "contact_typo": Unknown field.), + ~s(In field "default_with_stream": Unknown field.) + ], + run(@graphql, @schema, + variables: %{ + "contact" => %{ + "email" => "bubba@joe.com", + "default_with_stream" => "asdf", + "contact_typo" => "foo" + } + }, + maximum_number_of_suggestions: 0 + ) + ) end test "return field error with suggestion for non-null field" do @@ -202,5 +233,17 @@ defmodule Absinthe.Execution.Arguments.InputObjectTest do ], run(@graphql, @schema, variables: %{"contact" => %{"mail" => "bubba@joe.com"}}) ) + + assert_error_message_lines( + [ + ~s(Argument "contact" has invalid value $contact.), + ~s(In field "email": Expected type "String!", found null.), + ~s(In field "mail": Unknown field.) + ], + run(@graphql, @schema, + variables: %{"contact" => %{"mail" => "bubba@joe.com"}}, + maximum_number_of_suggestions: 0 + ) + ) end end diff --git a/test/absinthe/integration/validation/missing_selection_set_test.exs b/test/absinthe/integration/validation/missing_selection_set_test.exs index 9943d65007..212e8aaadd 100644 --- a/test/absinthe/integration/validation/missing_selection_set_test.exs +++ b/test/absinthe/integration/validation/missing_selection_set_test.exs @@ -18,5 +18,19 @@ defmodule Elixir.Absinthe.Integration.Validation.MissingSelectionSetTest do } ] }} == Absinthe.run(@query, Absinthe.Fixtures.Things.MacroSchema, []) + + assert {:ok, + %{ + errors: [ + %{ + message: + "Field \"things\" of type \"[Thing]\" must have a selection of subfields.", + locations: [%{column: 3, line: 2}] + } + ] + }} == + Absinthe.run(@query, Absinthe.Fixtures.Things.MacroSchema, + maximum_number_of_suggestions: 0 + ) end end diff --git a/test/absinthe/phase/document/validation/fields_on_correct_type_test.exs b/test/absinthe/phase/document/validation/fields_on_correct_type_test.exs index 64aaa62880..7245536165 100644 --- a/test/absinthe/phase/document/validation/fields_on_correct_type_test.exs +++ b/test/absinthe/phase/document/validation/fields_on_correct_type_test.exs @@ -7,10 +7,12 @@ defmodule Absinthe.Phase.Document.Validation.FieldsOnCorrectTypeTest do alias Absinthe.Blueprint + @options [] + defp undefined_field(name, type_name, type_suggestions, field_suggestions, line) do bad_value( Blueprint.Document.Field, - @phase.error_message(name, type_name, type_suggestions, field_suggestions), + @phase.error_message(name, type_name, type_suggestions, field_suggestions, @options), line, name: name ) @@ -259,32 +261,40 @@ defmodule Absinthe.Phase.Document.Validation.FieldsOnCorrectTypeTest do end test "fields on correct type error message: Works with no suggestions" do - assert ~s(Cannot query field "f" on type "T".) == @phase.error_message("f", "T", [], []) + assert ~s(Cannot query field "f" on type "T".) == + @phase.error_message("f", "T", [], [], @options) end test "fields on correct type error message: Works with no small numbers of type suggestions" do assert ~s(Cannot query field "f" on type "T". Did you mean to use an inline fragment on "A" or "B"?) == - @phase.error_message("f", "T", ["A", "B"], []) + @phase.error_message("f", "T", ["A", "B"], [], @options) end test "fields on correct type error message: Works with no small numbers of field suggestions" do assert ~s(Cannot query field "f" on type "T". Did you mean "z" or "y"?) == - @phase.error_message("f", "T", [], ["z", "y"]) + @phase.error_message("f", "T", [], ["z", "y"], @options) end test "fields on correct type error message: Only shows one set of suggestions at a time, preferring types" do assert ~s(Cannot query field "f" on type "T". Did you mean to use an inline fragment on "A" or "B"?) == - @phase.error_message("f", "T", ["A", "B"], ["z", "y"]) + @phase.error_message("f", "T", ["A", "B"], ["z", "y"], @options) end test "fields on correct type error message: Limits lots of type suggestions" do assert ~s(Cannot query field "f" on type "T". Did you mean to use an inline fragment on "A", "B", "C", "D", or "E"?) == - @phase.error_message("f", "T", ["A", "B", "C", "D", "E", "F"], []) + @phase.error_message("f", "T", ["A", "B", "C", "D", "E", "F"], [], @options) end test "fields on correct type error message: Limits lots of field suggestions" do assert ~s(Cannot query field "f" on type "T". Did you mean "z", "y", "x", "w", or "v"?) == - @phase.error_message("f", "T", [], ["z", "y", "x", "w", "v", "u"]) + @phase.error_message("f", "T", [], ["z", "y", "x", "w", "v", "u"], @options) + end + + test "fields on correct type error message: without suggestions" do + assert ~s(Cannot query field "f" on type "T".) == + @phase.error_message("f", "T", [], ["z", "y", "x", "w", "v", "u"], + maximum_number_of_suggestions: 0 + ) end end end diff --git a/test/absinthe/phase/document/validation/scalar_leafs_test.exs b/test/absinthe/phase/document/validation/scalar_leafs_test.exs index 65f678e8ba..20258fc3b5 100644 --- a/test/absinthe/phase/document/validation/scalar_leafs_test.exs +++ b/test/absinthe/phase/document/validation/scalar_leafs_test.exs @@ -19,7 +19,7 @@ defmodule Absinthe.Phase.Document.Validation.ScalarLeafsTest do defp missing_obj_subselection(node_name, type_name, line) do bad_value( Blueprint.Document.Field, - @phase.required_subselection_message(node_name, type_name), + @phase.required_subselection_message(node_name, type_name, []), line, name: node_name ) diff --git a/test/absinthe/strict_schema_test.exs b/test/absinthe/strict_schema_test.exs index d62592a0bf..d1c8fcd0c4 100644 --- a/test/absinthe/strict_schema_test.exs +++ b/test/absinthe/strict_schema_test.exs @@ -164,6 +164,17 @@ defmodule Absinthe.StrictSchemaTest do variables: variables ) ) + + assert_error_message( + "Cannot query field \"foo_bar_query\" on type \"RootQueryType\".", + run( + document, + Absinthe.Fixtures.StrictSchema, + adapter: Absinthe.Adapter.StrictLanguageConventions, + variables: variables, + maximum_number_of_suggestions: 0 + ) + ) end test "returns an error when underscore external name used in argument" do @@ -288,6 +299,15 @@ defmodule Absinthe.StrictSchemaTest do variables: variables ) ) + + assert_error_message( + "Cannot query field \"foo_bar_mutation\" on type \"RootMutationType\".", + run(document, Absinthe.Fixtures.StrictSchema, + adapter: Absinthe.Adapter.StrictLanguageConventions, + variables: variables, + maximum_number_of_suggestions: 0 + ) + ) end test "returns an error when underscore external name used in argument" do diff --git a/test/absinthe/type/interface_test.exs b/test/absinthe/type/interface_test.exs index 857ef6c703..f8c2bed916 100644 --- a/test/absinthe/type/interface_test.exs +++ b/test/absinthe/type/interface_test.exs @@ -107,6 +107,11 @@ defmodule Absinthe.Type.InterfaceTest do ~s(Cannot query field "age" on type "NamedEntity". Did you mean to use an inline fragment on "Person"?), run(@graphql, Absinthe.Fixtures.ContactSchema) ) + + assert_error_message( + ~s(Cannot query field "age" on type "NamedEntity".), + run(@graphql, Absinthe.Fixtures.ContactSchema, maximum_number_of_suggestions: 0) + ) end @graphql """ @@ -259,6 +264,11 @@ defmodule Absinthe.Type.InterfaceTest do ~s(Cannot query field "cost" on type "Item". Did you mean to use an inline fragment on "ValuedItem"?), run(@graphql, InterfaceSchema) ) + + assert_error_message( + ~s(Cannot query field "cost" on type "Item".), + run(@graphql, InterfaceSchema, maximum_number_of_suggestions: 0) + ) end @graphql """