diff --git a/config/config.exs b/config/config.exs index b541d3c38b..00a8736f5e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -4,6 +4,9 @@ use Mix.Config config :logger, level: :debug +# Results will be ordered lists of tuples instead of maps +# config :absinthe, ordered: true + # This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this # file won't be loaded nor affect the parent project. For this reason, diff --git a/lib/absinthe.ex b/lib/absinthe.ex index 8bad8fa7ca..33b09c0215 100644 --- a/lib/absinthe.ex +++ b/lib/absinthe.ex @@ -85,7 +85,7 @@ defmodule Absinthe do root_value: term, operation_name: String.t, analyze_complexity: boolean, - max_complexity: non_neg_integer | :infinity + max_complexity: non_neg_integer | :infinity, ] @type run_result :: {:ok, result_t} | {:error, String.t} diff --git a/lib/absinthe/middleware.ex b/lib/absinthe/middleware.ex index 694a390ae8..cb680ba4b4 100644 --- a/lib/absinthe/middleware.ex +++ b/lib/absinthe/middleware.ex @@ -235,7 +235,7 @@ defmodule Absinthe.Middleware do **It is important to note that we are matching for the `:query`, `:subscription` or `:mutation` identifier types. We do this because the middleware function will be called for each field in the schema. It is also important to provide a fallback so - that the default `Absinthe.Middleware.MapGet` is configured.** + that the default `Absinthe.Middleware.MapGet` or `Absinthe.Middleware.OrdMapGet` is configured.** ## Main Points diff --git a/lib/absinthe/middleware/map_get.ex b/lib/absinthe/middleware/map_get.ex index 39b99abd81..20a9a9cb8a 100644 --- a/lib/absinthe/middleware/map_get.ex +++ b/lib/absinthe/middleware/map_get.ex @@ -8,4 +8,4 @@ defmodule Absinthe.Middleware.MapGet do def call(%{source: source} = res, key) do %{res | state: :resolved, value: Map.get(source, key)} end -end +end \ No newline at end of file diff --git a/lib/absinthe/middleware/ord_map_get.ex b/lib/absinthe/middleware/ord_map_get.ex new file mode 100644 index 0000000000..4cbaa23d76 --- /dev/null +++ b/lib/absinthe/middleware/ord_map_get.ex @@ -0,0 +1,11 @@ +defmodule Absinthe.Middleware.OrdMapGet do + @moduledoc """ + This is middleware for ordered results. + """ + + @behaviour Absinthe.Middleware + + def call(%{source: source} = res, key) do + %{res | state: :resolved, value: OrdMap.get(OrdMap.new(source), key)} + end +end diff --git a/lib/absinthe/phase/document/ordered_result.ex b/lib/absinthe/phase/document/ordered_result.ex new file mode 100644 index 0000000000..4a8b81629d --- /dev/null +++ b/lib/absinthe/phase/document/ordered_result.ex @@ -0,0 +1,120 @@ +defmodule Absinthe.Phase.Document.OrderedResult do + + @moduledoc false + + # Produces data fit for external encoding from annotated value tree + # Preserves fields order + + alias Absinthe.{Blueprint, Phase, Type} + use Absinthe.Phase + + @spec run(Blueprint.t | Phase.Error.t, Keyword.t) :: {:ok, map} + def run(%Blueprint{} = bp, _options \\ []) do + {:ok, %{bp | result: process(bp)}} + end + + defp process(blueprint) do + result = case blueprint.execution do + %{validation_errors: [], result: nil} -> + :execution_failed + %{validation_errors: [], result: result} -> + {:ok, field_data(result.fields, [])} + %{validation_errors: errors} -> + {:validation_failed, errors} + end + format_result(result) + end + + defp format_result(:execution_failed) do + %{data: nil} + end + defp format_result({:ok, {data, []}}) do + %{data: data} + end + defp format_result({:ok, {data, errors}}) do + errors = errors |> Enum.uniq |> Enum.map(&format_error/1) + %{data: data, errors: errors} + end + defp format_result({:validation_failed, errors}) do + errors = errors |> Enum.uniq |> Enum.map(&format_error/1) + %{errors: errors} + end + defp format_result({:parse_failed, error}) do + %{errors: [format_error(error)]} + end + + defp data(%{errors: [_|_] = field_errors}, errors), do: {nil, field_errors ++ errors} + + # Leaf + defp data(%{value: nil}, errors), do: {nil, errors} + defp data(%{value: value, emitter: emitter}, errors) do + value = + case Type.unwrap(emitter.schema_node.type) do + %Type.Scalar{} = schema_node -> + Type.Scalar.serialize(schema_node, value) + %Type.Enum{} = schema_node -> + Type.Enum.serialize(schema_node, value) + end + {value, errors} + end + + # Object + defp data(%{fields: fields}, errors), do: field_data(fields, errors) + + # List + defp data(%{values: values}, errors), do: list_data(values, errors) + + defp list_data(fields, errors, acc \\ []) + defp list_data([], errors, acc), do: {:lists.reverse(acc), errors} + defp list_data([%{errors: []} = field | fields], errors, acc) do + {value, errors} = data(field, errors) + list_data(fields, errors, [value | acc]) + end + defp list_data([%{errors: errs} | fields], errors, acc) when length(errs) > 0 do + list_data(fields, errs ++ errors, acc) + end + + defp field_data(fields, errors, acc \\ []) + defp field_data([], errors, acc) do + {OrdMap.new(:lists.reverse(acc)), errors} + end + defp field_data([%Absinthe.Resolution{} = res | _], _errors, _acc) do + raise """ + Found unresolved resolution struct! + + You probably forgot to run the resolution phase again. + + #{inspect res} + """ + end + defp field_data([field | fields], errors, acc) do + {value, errors} = data(field, errors) + field_data(fields, errors, [{field_name(field.emitter), value} | acc]) + end + + defp field_name(%{alias: nil, name: name}), do: name + defp field_name(%{alias: name}), do: name + defp field_name(%{name: name}), do: name + + defp format_error(%Phase.Error{locations: []} = error) do + error_object = %{message: error.message} + Map.merge(error.extra, error_object) + end + defp format_error(%Phase.Error{} = error) do + error_object = %{ + message: error.message, + locations: Enum.flat_map(error.locations, &format_location/1), + } + error_object = case error.path do + [] -> error_object + path -> Map.put(error_object, :path, path) + end + Map.merge(Map.new(error.extra), error_object) + end + + defp format_location(%{line: line, column: col}) do + [%{line: line || 0, column: col || 0}] + end + defp format_location(_), do: [] + +end diff --git a/lib/absinthe/phase/document/result.ex b/lib/absinthe/phase/document/result.ex index 9c47fd7532..0fab652ddb 100644 --- a/lib/absinthe/phase/document/result.ex +++ b/lib/absinthe/phase/document/result.ex @@ -1,117 +1,113 @@ defmodule Absinthe.Phase.Document.Result do - - @moduledoc false - - # Produces data fit for external encoding from annotated value tree - - alias Absinthe.{Blueprint, Phase, Type} - use Absinthe.Phase - - @spec run(Blueprint.t | Phase.Error.t, Keyword.t) :: {:ok, map} - def run(%Blueprint{} = bp, _options \\ []) do - {:ok, %{bp | result: process(bp)}} - end - - defp process(blueprint) do - result = case blueprint.execution do - %{validation_errors: [], result: nil} -> - :execution_failed - %{validation_errors: [], result: result} -> - {:ok, field_data(result.fields, [])} - %{validation_errors: errors} -> - {:validation_failed, errors} + + @moduledoc false + + # Produces data fit for external encoding from annotated value tree + + alias Absinthe.{Blueprint, Phase, Type} + use Absinthe.Phase + + @spec run(Blueprint.t | Phase.Error.t, Keyword.t) :: {:ok, map} + def run(%Blueprint{} = bp, _options \\ []) do + result = Map.merge(bp.result, process(bp)) + {:ok, %{bp | result: result}} end - format_result(result) - end - - defp format_result(:execution_failed) do - %{data: nil} - end - defp format_result({:ok, {data, []}}) do - %{data: data} - end - defp format_result({:ok, {data, errors}}) do - errors = errors |> Enum.uniq |> Enum.map(&format_error/1) - %{data: data, errors: errors} - end - defp format_result({:validation_failed, errors}) do - errors = errors |> Enum.uniq |> Enum.map(&format_error/1) - %{errors: errors} - end - defp format_result({:parse_failed, error}) do - %{errors: [format_error(error)]} - end - - defp data(%{errors: [_|_] = field_errors}, errors), do: {nil, field_errors ++ errors} - - # Leaf - defp data(%{value: nil}, errors), do: {nil, errors} - defp data(%{value: value, emitter: emitter}, errors) do - value = - case Type.unwrap(emitter.schema_node.type) do - %Type.Scalar{} = schema_node -> - Type.Scalar.serialize(schema_node, value) - %Type.Enum{} = schema_node -> - Type.Enum.serialize(schema_node, value) + + defp process(blueprint) do + result = case blueprint.execution do + %{validation_errors: [], result: result} -> + {:ok, data(result, [])} + %{validation_errors: errors} -> + {:validation_failed, errors} end - {value, errors} - end - - # Object - defp data(%{fields: fields}, errors), do: field_data(fields, errors) - - # List - defp data(%{values: values}, errors), do: list_data(values, errors) - - defp list_data(fields, errors, acc \\ []) - defp list_data([], errors, acc), do: {:lists.reverse(acc), errors} - defp list_data([%{errors: []} = field | fields], errors, acc) do - {value, errors} = data(field, errors) - list_data(fields, errors, [value | acc]) - end - defp list_data([%{errors: errs} | fields], errors, acc) when length(errs) > 0 do - list_data(fields, errs ++ errors, acc) - end - - defp field_data(fields, errors, acc \\ []) - defp field_data([], errors, acc), do: {Map.new(acc), errors} - defp field_data([%Absinthe.Resolution{} = res | _], _errors, _acc) do - raise """ - Found unresolved resolution struct! - - You probably forgot to run the resolution phase again. - - #{inspect res} - """ - end - defp field_data([field | fields], errors, acc) do - {value, errors} = data(field, errors) - field_data(fields, errors, [{field_name(field.emitter), value} | acc]) - end - - defp field_name(%{alias: nil, name: name}), do: name - defp field_name(%{alias: name}), do: name - defp field_name(%{name: name}), do: name - - defp format_error(%Phase.Error{locations: []} = error) do - error_object = %{message: error.message} - Map.merge(error.extra, error_object) - end - defp format_error(%Phase.Error{} = error) do - error_object = %{ - message: error.message, - locations: Enum.flat_map(error.locations, &format_location/1), - } - error_object = case error.path do - [] -> error_object - path -> Map.put(error_object, :path, path) + format_result(result) end - Map.merge(Map.new(error.extra), error_object) - end - - defp format_location(%{line: line, column: col}) do - [%{line: line || 0, column: col || 0}] - end - defp format_location(_), do: [] - -end + + defp format_result(:execution_failed) do + %{data: nil} + end + defp format_result({:ok, {data, []}}) do + %{data: data} + end + defp format_result({:ok, {data, errors}}) do + errors = errors |> Enum.uniq |> Enum.map(&format_error/1) + %{data: data, errors: errors} + end + defp format_result({:validation_failed, errors}) do + errors = errors |> Enum.uniq |> Enum.map(&format_error/1) + %{errors: errors} + end + defp format_result({:parse_failed, error}) do + %{errors: [format_error(error)]} + end + + defp data(%{errors: [_|_] = field_errors}, errors), do: {nil, field_errors ++ errors} + + # Leaf + defp data(%{value: nil}, errors), do: {nil, errors} + defp data(%{value: value, emitter: emitter}, errors) do + value = + case Type.unwrap(emitter.schema_node.type) do + %Type.Scalar{} = schema_node -> + Type.Scalar.serialize(schema_node, value) + %Type.Enum{} = schema_node -> + Type.Enum.serialize(schema_node, value) + end + {value, errors} + end + + # Object + defp data(%{fields: fields}, errors), do: field_data(fields, errors) + + # List + defp data(%{values: values}, errors), do: list_data(values, errors) + + defp list_data(fields, errors, acc \\ []) + defp list_data([], errors, acc), do: {:lists.reverse(acc), errors} + defp list_data([%{errors: errs} = field | fields], errors, acc) do + {value, errors} = data(field, errors) + list_data(fields, errs ++ errors, [value | acc]) + end + + defp field_data(fields, errors, acc \\ []) + defp field_data([], errors, acc), do: {Map.new(acc), errors} + defp field_data([%Absinthe.Resolution{} = res | _], _errors, _acc) do + raise """ + Found unresolved resolution struct! + + You probably forgot to run the resolution phase again. + + #{inspect res} + """ + end + defp field_data([field | fields], errors, acc) do + {value, errors} = data(field, errors) + field_data(fields, errors, [{field_name(field.emitter), value} | acc]) + end + + defp field_name(%{alias: nil, name: name}), do: name + defp field_name(%{alias: name}), do: name + defp field_name(%{name: name}), do: name + + defp format_error(%Phase.Error{locations: []} = error) do + error_object = %{message: error.message} + Map.merge(error.extra, error_object) + end + defp format_error(%Phase.Error{} = error) do + error_object = %{ + message: error.message, + locations: Enum.flat_map(error.locations, &format_location/1), + } + error_object = case error.path do + [] -> error_object + path -> Map.put(error_object, :path, path) + end + Map.merge(Map.new(error.extra), error_object) + end + + defp format_location(%{line: line, column: col}) do + [%{line: line || 0, column: col || 0}] + end + defp format_location(_), do: [] + + end \ No newline at end of file diff --git a/lib/absinthe/phase/subscription/subscribe_self.ex b/lib/absinthe/phase/subscription/subscribe_self.ex index 5f98cd520a..c6bee86033 100644 --- a/lib/absinthe/phase/subscription/subscribe_self.ex +++ b/lib/absinthe/phase/subscription/subscribe_self.ex @@ -33,7 +33,7 @@ defmodule Absinthe.Phase.Subscription.SubscribeSelf do blueprint = update_in(blueprint.execution.validation_errors, &[error | &1]) error_pipeline = [ - {Phase.Document.Result, options}, + {Absinthe.Utils.getDefaultDocumentResult(), options} ] {:replace, blueprint, error_pipeline} diff --git a/lib/absinthe/pipeline.ex b/lib/absinthe/pipeline.ex index 8d5988f9c3..54f92d5e9c 100644 --- a/lib/absinthe/pipeline.ex +++ b/lib/absinthe/pipeline.ex @@ -31,15 +31,16 @@ defmodule Absinthe.Pipeline do context: %{}, root_value: %{}, validation_result_phase: Phase.Document.Validation.Result, - result_phase: Phase.Document.Result, + result_phase: nil, jump_phases: true, ] @spec for_document(Absinthe.Schema.t) :: t @spec for_document(Absinthe.Schema.t, Keyword.t) :: t def for_document(schema, options \\ []) do - options = @defaults + options = @defaults |> Keyword.merge(Keyword.put(options, :schema, schema)) + |> Keyword.put(:result_phase, Keyword.get(options, :result_phase, Absinthe.Utils.getDefaultDocumentResult())) [ # Parse Document {Phase.Parse, options}, @@ -99,7 +100,7 @@ defmodule Absinthe.Pipeline do {Phase.Subscription.SubscribeSelf, options}, {Phase.Document.Execution.Resolution, options}, # Format Result - Phase.Document.Result + Absinthe.Utils.getDefaultDocumentResult() ] end diff --git a/lib/absinthe/schema.ex b/lib/absinthe/schema.ex index 2ee0a0901c..10bf28ff66 100644 --- a/lib/absinthe/schema.ex +++ b/lib/absinthe/schema.ex @@ -222,7 +222,7 @@ defmodule Absinthe.Schema do [Absinthe.Middleware.PassParent] end def __ensure_middleware__([], %{identifier: identifier}, _) do - [{Absinthe.Middleware.MapGet, identifier}] + [{Absinthe.Utils.getDefaultMiddleware(), identifier}] end def __ensure_middleware__(middleware, _field, _object) do middleware @@ -248,7 +248,8 @@ defmodule Absinthe.Schema do Replace the default for all fields with a string lookup instead of an atom lookup: ``` def middleware(middleware, field, object) do - new_middleware = {Absinthe.Middleware.MapGet, to_string(field.identifier)} + defaultMiddleware = Utils.getDefaultDocumentResult() + new_middleware = {^defaultMiddleware, to_string(field.identifier)} middleware |> Absinthe.Schema.replace_default(new_middleware, field, object) end @@ -257,9 +258,10 @@ defmodule Absinthe.Schema do ``` """ def replace_default(middleware_list, new_middleware, %{identifier: identifer}, _object) do + defaultMiddleware = Absinthe.Utils.getDefaultMiddleware() Enum.map(middleware_list, fn middleware -> case middleware do - {Absinthe.Middleware.MapGet, ^identifer} -> + {^defaultMiddleware, ^identifer} -> new_middleware middleware -> middleware end diff --git a/lib/absinthe/subscription/local.ex b/lib/absinthe/subscription/local.ex index 5f5924aa5f..6911d54892 100644 --- a/lib/absinthe/subscription/local.ex +++ b/lib/absinthe/subscription/local.ex @@ -18,7 +18,7 @@ defmodule Absinthe.Subscription.Local do {topics, docs} = Enum.unzip(docs_and_topics) docs = BatchResolver.run(docs, [schema: hd(docs).schema, abort_on_error: false]) pipeline = [ - Absinthe.Phase.Document.Result + Absinthe.Utils.getDefaultDocumentResult() ] for {doc, {topic, key_strategy}} <- Enum.zip(docs, topics), doc != :error do try do diff --git a/lib/absinthe/utils.ex b/lib/absinthe/utils.ex index 3a0118ff39..8f3c853899 100644 --- a/lib/absinthe/utils.ex +++ b/lib/absinthe/utils.ex @@ -119,4 +119,17 @@ defmodule Absinthe.Utils do """ end + @doc false + def getDefaultMiddleware() do + # IO.inspect Application.get_env(:absinthe, :ordered), label: "ENV__ORDERD_MIDDLEWARE" + ordered = Application.get_env(:absinthe, :ordered, false) + unless ordered, do: Absinthe.Middleware.MapGet, else: Absinthe.Middleware.OrdMapGet + end + + @doc false + def getDefaultDocumentResult() do + # IO.inspect Application.get_env(:absinthe, :ordered), label: "ENV__ORDERD_DOC" + ordered = Application.get_env(:absinthe, :ordered, false) + unless ordered, do: Absinthe.Phase.Document.Result, else: Absinthe.Phase.Document.OrderedResult + end end diff --git a/mix.exs b/mix.exs index 9e55550ed9..f74bb91a57 100644 --- a/mix.exs +++ b/mix.exs @@ -54,7 +54,8 @@ defmodule Absinthe.Mixfile do {:dialyze, "~> 0.2", only: :dev}, {:decimal, "~> 1.0", optional: :true}, {:phoenix_pubsub, ">= 0.0.0", only: :test}, - {:mix_test_watch, "~> 0.4.1", only: [:test, :dev]} + {:mix_test_watch, "~> 0.4.1", only: [:test, :dev]}, + {:ord_map, "~> 0.1.0"} ] end @@ -146,6 +147,7 @@ defmodule Absinthe.Mixfile do Absinthe.Middleware.Batch, Absinthe.Middleware.Dataloader, Absinthe.Middleware.MapGet, + Absinthe.Middleware.OrdMapGet, Absinthe.Middleware.PassParent ], diff --git a/mix.lock b/mix.lock index 78ead9b109..9d32c690ee 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ -%{"benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm"}, +%{ + "benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm"}, "dataloader": {:git, "https://github.com/absinthe-graphql/dataloader.git", "8fa22ae628efdec4667a4c70b57c9f261d498f06", []}, "decimal": {:hex, :decimal, "1.4.1", "ad9e501edf7322f122f7fc151cce7c2a0c9ada96f2b0155b8a09a795c2029770", [:mix], [], "hexpm"}, "dialyze": {:hex, :dialyze, "0.2.1", "9fb71767f96649020d769db7cbd7290059daff23707d6e851e206b1fdfa92f9d", [], [], "hexpm"}, @@ -7,4 +8,6 @@ "ex_spec": {:hex, :ex_spec, "2.0.1", "8bdbd6fa85995fbf836ed799571d44be6f9ebbcace075209fd0ad06372c111cf", [:mix], [], "hexpm"}, "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], [], "hexpm"}, "mix_test_watch": {:hex, :mix_test_watch, "0.4.1", "a98a84c795623f1ba020324f4354cf30e7120ba4dab65f9c2ae300f830a25f75", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, repo: "hexpm", optional: false]}], "hexpm"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"}} + "ord_map": {:hex, :ord_map, "0.1.0", "6c958f53e38934a2f60d4b0050e3bdfcc498071769f93887372ae4db5cb21d3c", [], [], "hexpm"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"}, +} diff --git a/test/graphql_specification/null_values/input_object_test.exs b/test/graphql_specification/null_values/input_object_test.exs index 8607b84e5b..d8aad993d3 100644 --- a/test/graphql_specification/null_values/input_object_test.exs +++ b/test/graphql_specification/null_values/input_object_test.exs @@ -1,8 +1,7 @@ defmodule GraphQL.Specification.NullValues.InputObjectTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false import AssertResult - defmodule Schema do use Absinthe.Schema diff --git a/test/graphql_specification/null_values/list_test.exs b/test/graphql_specification/null_values/list_test.exs index 9ff2393160..0bb5bf066d 100644 --- a/test/graphql_specification/null_values/list_test.exs +++ b/test/graphql_specification/null_values/list_test.exs @@ -1,5 +1,5 @@ defmodule GraphQL.Specification.NullValues.ListTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false import AssertResult defmodule Schema do diff --git a/test/graphql_specification/null_values/ordered_input_object_test.exs b/test/graphql_specification/null_values/ordered_input_object_test.exs new file mode 100644 index 0000000000..d3e55f8301 --- /dev/null +++ b/test/graphql_specification/null_values/ordered_input_object_test.exs @@ -0,0 +1,109 @@ +defmodule GraphQL.Specification.NullValues.OrderedInputObjectTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + import AssertResult + + defmodule Schema do + use Absinthe.Schema + + query do + + field :obj_times, :integer do + arg :input, non_null(:times_input) + resolve fn + _, %{input: %{base: base, multiplier: nil}}, _ -> + {:ok, base} + _, %{input: %{base: base, multiplier: num}}, _ -> + {:ok, base * num} + end + end + + end + + input_object :times_input do + field :multiplier, :integer, default_value: 2 + field :base, non_null(:integer) + end + + end + + context "as a literal" do + + context "to a nullable input object field with a default value" do + + context "(control): if not passed" do + + @query """ + { times: objTimes(input: {base: 4}) } + """ + it "uses the default value" do + assert_result {:ok, %{data: o%{"times" => 8}}}, run(@query, Schema) + end + + end + + context "if passed" do + + @query """ + { times: objTimes(input: {base: 4, multiplier: null}) } + """ + it "overrides the default and is passed as nil to the resolver" do + assert_result {:ok, %{data: o%{"times" => 4}}}, run(@query, Schema) + end + + end + + end + + context "to a non-nullable input object field" do + + context "if passed" do + + @query """ + { times: objTimes(input: {base: null}) } + """ + it "adds an error" do + assert_result {:ok, %{errors: [%{message: "Argument \"input\" has invalid value {base: null}.\nIn field \"base\": Expected type \"Int!\", found null."}]}}, run(@query, Schema) + end + + end + + end + + end + + context "as a variable" do + + context "to a nullable input object field with a default value" do + + context "if passed" do + + @query """ + query ($value: Int) { times: objTimes(input: {base: 4, multiplier: $value}) } + """ + it "overrides the default and is passed as nil to the resolver" do + assert_result {:ok, %{data: o%{"times" => 4}}}, run(@query, Schema, variables: %{"value" => nil}) + end + + end + + end + + context "to a non-nullable input object field" do + + context "if passed" do + + @query """ + query ($value: Int!) { times: objTimes(input: {base: $value}) } + """ + it "adds an error" do + assert_result {:ok, %{errors: [%{message: "Argument \"input\" has invalid value {base: $value}.\nIn field \"base\": Expected type \"Int!\", found $value."}, %{message: "Variable \"value\": Expected non-null, found null."}]}}, run(@query, Schema, variables: %{"value" => nil}) + end + + end + + end + + end + +end diff --git a/test/graphql_specification/null_values/ordered_list_test.exs b/test/graphql_specification/null_values/ordered_list_test.exs new file mode 100644 index 0000000000..32d3224fed --- /dev/null +++ b/test/graphql_specification/null_values/ordered_list_test.exs @@ -0,0 +1,511 @@ +defmodule GraphQL.Specification.NullValues.OrderedListTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + import AssertResult + + defmodule Schema do + use Absinthe.Schema + + query do + + field :nullable_list, :list_details do + arg :input, list_of(:integer) + resolve fn + _, %{input: nil}, _ -> + {:ok, nil} + _, %{input: list}, _ -> + { + :ok, + %{ + length: length(list), + content: list, + null_count: Enum.count(list, &(&1 == nil)), + non_null_count: Enum.count(list, &(&1 != nil)), + } + } + end + end + + field :non_nullable_list, :list_details do + arg :input, non_null(list_of(:integer)) + resolve fn + _, %{input: list}, _ -> + { + :ok, + %{ + length: length(list), + content: list, + null_count: Enum.count(list, &(&1 == nil)), + non_null_count: Enum.count(list, &(&1 != nil)), + } + } + end + end + + field :nullable_list_of_non_nullable_type, :list_details do + arg :input, list_of(non_null(:integer)) + resolve fn + _, %{input: nil}, _ -> + {:ok, nil} + _, %{input: list}, _ -> + { + :ok, + %{ + length: length(list), + content: list, + null_count: Enum.count(list, &(&1 == nil)), + non_null_count: Enum.count(list, &(&1 != nil)), + } + } + end + end + + field :non_nullable_list_of_non_nullable_type, :list_details do + arg :input, non_null(list_of(non_null(:integer))) + resolve fn + _, %{input: list}, _ -> + { + :ok, + %{ + length: length(list), + content: list, + null_count: Enum.count(list, &(&1 == nil)), + non_null_count: Enum.count(list, &(&1 != nil)), + } + } + end + end + + end + + object :list_details do + field :length, :integer + field :content, list_of(:integer) + field :null_count, :integer + field :non_null_count, :integer + end + + end + + context "as a literal" do + + context "to an [Int]" do + + context "if passed as the value" do + @query """ + { + nullableList(input: null) { + length + content + nonNullCount + nullCount + } + } + """ + it "is treated as a null argument" do + assert_result { + :ok, + %{ + data: o%{ + "nullableList" => nil + } + } + }, run(@query, Schema) + end + end + + context "if passed as an element" do + @query """ + { + nullableList(input: [null, 1]) { + length + content + nullCount + nonNullCount + } + } + """ + it "is treated as a valid value" do + assert_result { + :ok, + %{ + data: o%{ + "nullableList" => o%{ + "length" => 2, + "content" => [nil, 1], + "nullCount" => 1, + "nonNullCount" => 1 + } + } + } + }, run(@query, Schema) + end + end + + end + + context "to an [Int]!" do + + context "if passed as the value" do + @query """ + { + nonNullableList(input: null) { + length + content + nonNullCount + nullCount + } + } + """ + it "is treated as a null argument" do + assert_result { + :ok, + %{ + errors: [%{message: "Argument \"input\" has invalid value null."}] + } + }, run(@query, Schema) + end + end + + context "if passed as an element" do + @query """ + { + nonNullableList(input: [null, 1]) { + length + content + nullCount + nonNullCount + } + } + """ + it "is treated as a valid value" do + assert_result { + :ok, + %{ + data: o%{ + "nonNullableList" => o%{ + "length" => 2, + "content" => [nil, 1], + "nullCount" => 1, + "nonNullCount" => 1 + } + } + } + }, run(@query, Schema) + end + end + + end + + context "to an [Int!]" do + + context "if passed as the value" do + @query """ + { + nullableListOfNonNullableType(input: null) { + length + content + nonNullCount + nullCount + } + } + """ + it "is treated as a null argument" do + assert_result { + :ok, + %{ + data: o%{ + "nullableListOfNonNullableType" => nil + } + } + }, run(@query, Schema) + end + end + + context "if passed as an element" do + @query """ + { + nullableListOfNonNullableType(input: [null, 1]) { + length + content + nonNullCount + nullCount + } + } + """ + it "returns an error" do + assert_result { + :ok, + %{ + errors: [ + %{message: "Argument \"input\" has invalid value [null, 1].\nIn element #1: Expected type \"Int!\", found null."} + ] + } + }, run(@query, Schema) + end + end + + end + + context "to an [Int!]!" do + + context "if passed as the value" do + @query """ + { + nonNullableListOfNonNullableType(input: null) { + length + content + nonNullCount + nullCount + } + } + """ + it "is treated as a null argument" do + assert_result { + :ok, + %{ + errors: [%{message: "Argument \"input\" has invalid value null."}] + } + }, run(@query, Schema) + end + end + + context "if passed as an element" do + @query """ + { + nonNullableListOfNonNullableType(input: [null, 1]) { + length + content + nonNullCount + nullCount + } + } + """ + it "returns an error" do + assert_result { + :ok, + %{ + errors: [ + %{message: "Argument \"input\" has invalid value [null, 1].\nIn element #1: Expected type \"Int!\", found null."} + ] + } + }, run(@query, Schema) + end + end + + end + + end + + context "as a variable" do + + context "to an [Int]" do + + context "if passed as the value" do + @query """ + query ($value: [Int]) { + nullableList(input: $value) { + length + content + nonNullCount + nullCount + } + } + """ + it "is treated as a null argument" do + assert_result { + :ok, + %{ + data: o%{ + "nullableList" => nil + } + } + }, run(@query, Schema, variables: %{"value" => nil}) + end + end + + context "if passed as an element" do + @query """ + query ($value: [Int] ){ + nullableList(input: $value) { + length + content + nullCount + nonNullCount + } + } + """ + it "is treated as a valid value" do + assert_result { + :ok, + %{ + data: o%{ + "nullableList" => o%{ + "length" => 2, + "content" => [nil, 1], + "nullCount" => 1, + "nonNullCount" => 1 + } + } + } + }, run(@query, Schema, variables: %{"value" => [nil, 1]}) + end + end + + end + + context "to an [Int]!" do + + context "if passed as the value" do + @query """ + query ($value: [Int]!) { + nonNullableList(input: $value) { + length + content + nonNullCount + nullCount + } + } + """ + it "is treated as a null argument" do + assert_result { + :ok, + %{ + errors: [%{message: "Argument \"input\" has invalid value $value."}, %{message: "Variable \"value\": Expected non-null, found null."}] + } + }, run(@query, Schema, variables: %{"value" => nil}) + end + end + + context "if passed as an element" do + @query """ + query ($value: [Int]!){ + nonNullableList(input: $value) { + length + content + nullCount + nonNullCount + } + } + """ + it "is treated as a valid value" do + assert_result { + :ok, + %{ + data: o%{ + "nonNullableList" => o%{ + "length" => 2, + "content" => [nil, 1], + "nullCount" => 1, + "nonNullCount" => 1 + } + } + } + }, run(@query, Schema, variables: %{"value" => [nil, 1]}) + end + end + + end + + context "to an [Int!]" do + + context "if passed as the value" do + @query """ + query ($value: [Int!]) { + nullableListOfNonNullableType(input: $value) { + length + content + nonNullCount + nullCount + } + } + """ + it "is treated as a null argument" do + assert_result { + :ok, + %{ + data: o%{ + "nullableListOfNonNullableType" => nil + } + } + }, run(@query, Schema, variables: %{"value" => nil}) + end + end + + context "if passed as an element" do + @query """ + query ($value: [Int!]){ + nullableListOfNonNullableType(input: $value) { + length + content + nonNullCount + nullCount + } + } + """ + it "returns an error" do + assert_result { + :ok, + %{ + errors: [ + %{message: "Argument \"input\" has invalid value $value.\nIn element #1: Expected type \"Int!\", found null."} + ] + } + }, run(@query, Schema, variables: %{"value" => [nil, 1]}) + end + end + + end + + context "to an [Int!]!" do + + context "if passed as the value" do + @query """ + query ($value: [Int!]!){ + nonNullableListOfNonNullableType(input: $value) { + length + content + nonNullCount + nullCount + } + } + """ + it "is treated as a null argument" do + assert_result { + :ok, + %{ + errors: [%{message: "Argument \"input\" has invalid value $value."}, %{message: "Variable \"value\": Expected non-null, found null."}] + } + }, run(@query, Schema, variables: %{"value" => nil}) + end + end + + context "if passed as an element" do + @query """ + query ($value: [Int!]!) { + nonNullableListOfNonNullableType(input: $value) { + length + content + nonNullCount + nullCount + } + } + """ + @tag :check + it "returns an error" do + assert_result { + :ok, + %{ + errors: [ + %{message: "Argument \"input\" has invalid value $value.\nIn element #1: Expected type \"Int!\", found null."} + ] + } + }, run(@query, Schema, variables: %{"value" => [nil, 1]}) + end + end + + end + + end + +end diff --git a/test/graphql_specification/null_values/ordered_scalar_test.exs b/test/graphql_specification/null_values/ordered_scalar_test.exs new file mode 100644 index 0000000000..3a7372b706 --- /dev/null +++ b/test/graphql_specification/null_values/ordered_scalar_test.exs @@ -0,0 +1,121 @@ +defmodule GraphQL.Specification.NullValues.OrderedScalarTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + import AssertResult + + defmodule Schema do + use Absinthe.Schema + + query do + + field :times, :integer do + + arg :multiplier, :integer, default_value: 2 + arg :base, non_null(:integer) + + resolve fn + _, %{base: base, multiplier: nil}, _ -> + {:ok, base} + _, %{base: base, multiplier: num}, _ -> + {:ok, base * num} + _, %{base: _}, _ -> + {:error, "Didn't get any multiplier"} + end + + end + + end + + end + + context "as a literal" do + + context "to a nullable argument with a default value" do + + context "(control): if not passed" do + + @query """ + { times(base: 4) } + """ + it "uses the default value" do + assert_result {:ok, %{data: o%{"times" => 8}}}, run(@query, Schema) + end + + end + + context "if passed" do + + @query """ + { times(base: 4, multiplier: null) } + """ + it "overrides the default and is passed as nil to the resolver" do + assert_result {:ok, %{data: o%{"times" => 4}}}, run(@query, Schema) + end + + end + + end + + context "to a non-nullable argument" do + + context "if passed" do + + @query """ + { times(base: null) } + """ + it "adds an error" do + assert_result {:ok, %{errors: [%{message: "Argument \"base\" has invalid value null."}]}}, run(@query, Schema) + end + + end + + end + + end + + context "as a variable value" do + + context "to a variable with a default value" do + + context "if not passed (control)" do + + @query """ + query Test($mult: Int = 6) { times(base: 4, multiplier: $mult) } + """ + it "uses the default variable value" do + assert_result {:ok, %{data: o%{"times" => 24}}}, run(@query, Schema) + end + + end + + context "if passed" do + + @query """ + query Test($mult: Int = 6) { times(base: 4, multiplier: $mult) } + """ + it "overrides the default and is passed as nil to the resolver" do + assert_result {:ok, %{data: o%{"times" => 4}}}, run(@query, Schema, variables: %{"mult" => nil}) + end + + end + + end + + context "to a non-nullable variable" do + + context "if passed" do + + @query """ + query Test($mult: Int!) { times(base: 4, multiplier: $mult) } + """ + it "adds an error" do + assert_result {:ok, %{errors: [%{message: "Variable \"mult\": Expected non-null, found null."}]}}, run(@query, Schema, variables: %{"mult" => nil}) + end + + end + + end + + end + +end diff --git a/test/graphql_specification/null_values/scalar_test.exs b/test/graphql_specification/null_values/scalar_test.exs index 16f672b5fe..f71969846b 100644 --- a/test/graphql_specification/null_values/scalar_test.exs +++ b/test/graphql_specification/null_values/scalar_test.exs @@ -1,5 +1,5 @@ defmodule GraphQL.Specification.NullValues.ScalarTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false import AssertResult defmodule Schema do diff --git a/test/lib/absinthe/alias_test.exs b/test/lib/absinthe/alias_test.exs index b976fcacc1..f62e5d922c 100644 --- a/test/lib/absinthe/alias_test.exs +++ b/test/lib/absinthe/alias_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.Middleware.AliasTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false defmodule Schema do use Absinthe.Schema diff --git a/test/lib/absinthe/custom_types_test.exs b/test/lib/absinthe/custom_types_test.exs index f047e37ff0..bf3f0ff894 100644 --- a/test/lib/absinthe/custom_types_test.exs +++ b/test/lib/absinthe/custom_types_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.CustomTypesTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false import AssertResult defmodule Schema do diff --git a/test/lib/absinthe/execution/arguments_test.exs b/test/lib/absinthe/execution/arguments_test.exs index bd8bd17fe7..22e89ac332 100644 --- a/test/lib/absinthe/execution/arguments_test.exs +++ b/test/lib/absinthe/execution/arguments_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.Execution.ArgumentsTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false import AssertResult diff --git a/test/lib/absinthe/execution/default_resolver_test.exs b/test/lib/absinthe/execution/default_resolver_test.exs index dc7ea9094a..85d83d27b2 100644 --- a/test/lib/absinthe/execution/default_resolver_test.exs +++ b/test/lib/absinthe/execution/default_resolver_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.Execution.DefaultResolverTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false @root %{:foo => "baz", "bar" => "quux"} @query "{ foo bar }" diff --git a/test/lib/absinthe/execution/fragment_spread_test.exs b/test/lib/absinthe/execution/fragment_spread_test.exs index 342412d773..2d673059ff 100644 --- a/test/lib/absinthe/execution/fragment_spread_test.exs +++ b/test/lib/absinthe/execution/fragment_spread_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.Execution.FragmentSpreadTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false @query """ query AbstractFragmentSpread { diff --git a/test/lib/absinthe/execution/inline_fragments_test.exs b/test/lib/absinthe/execution/inline_fragments_test.exs index 15a156795d..a65f92e521 100644 --- a/test/lib/absinthe/execution/inline_fragments_test.exs +++ b/test/lib/absinthe/execution/inline_fragments_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.Execution.InlineFragmentsTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false @query """ { diff --git a/test/lib/absinthe/execution/list_test.exs b/test/lib/absinthe/execution/list_test.exs index 478439d9c5..83e2ae3eca 100644 --- a/test/lib/absinthe/execution/list_test.exs +++ b/test/lib/absinthe/execution/list_test.exs @@ -75,7 +75,7 @@ defmodule Absinthe.Execution.ListTest.Schema do end defmodule Absinthe.Execution.ListTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false @query """ { diff --git a/test/lib/absinthe/execution/ordered_arguments_test.exs b/test/lib/absinthe/execution/ordered_arguments_test.exs new file mode 100644 index 0000000000..4df669d990 --- /dev/null +++ b/test/lib/absinthe/execution/ordered_arguments_test.exs @@ -0,0 +1,460 @@ +defmodule Absinthe.Execution.OrderedArgumentsTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + import AssertResult + + defmodule Schema do + use Absinthe.Schema + + @res %{ + true => "YES", + false => "NO" + } + + scalar :input_name do + parse fn %{value: value} -> {:ok, %{first_name: value}} end + serialize fn %{first_name: name} -> name end + end + + scalar :name do + serialize &to_string/1 + parse fn + %Absinthe.Blueprint.Input.String{} = string -> + string.value + _ -> + :error + end + end + + input_object :boolean_input_object do + field :flag, :boolean + end + + input_object :contact_input do + field :email, non_null(:string) + field :contact_type, :contact_type + field :default_with_string, :string, default_value: "asdf" + field :nested_contact_input, :nested_contact_input + end + + input_object :nested_contact_input do + field :email, non_null(:string) + end + + enum :contact_type do + value :email, name: "Email", as: "Email" + value :phone + value :sms, deprecate: "Use phone instead" + end + + input_object :input_stuff do + field :value, :integer + field :non_null_field, non_null(:string) + end + + query do + field :stuff, :integer do + arg :stuff, non_null(:input_stuff) + resolve fn _, _ -> + {:ok, 14} + end + end + + field :test_boolean_input_object, :boolean do + arg :input, non_null(:boolean_input_object) + + resolve fn %{input: input}, _ -> + {:ok, input[:flag]} + end + end + + field :contact, :contact_type do + arg :type, :contact_type + + resolve fn args, _ -> {:ok, Map.get(args, :type)} end + end + + field :contacts, list_of(:string) do + arg :contacts, non_null(list_of(:contact_input)) + + resolve fn %{contacts: contacts}, _ -> + {:ok, Enum.map(contacts, &Map.get(&1, :email))} + end + end + + field :names, list_of(:input_name) do + arg :names, list_of(:input_name) + + resolve fn %{names: names}, _ -> {:ok, names} end + end + + field :list_of_lists, list_of(list_of(:string)) do + arg :items, list_of(list_of(:string)) + + resolve fn %{items: items}, _ -> + {:ok, items} + end + end + + field :numbers, list_of(:integer) do + arg :numbers, list_of(:integer) + + resolve fn + %{numbers: numbers}, _ -> + {:ok, numbers} + end + end + + field :user, :string do + arg :contact, :contact_input + resolve fn + %{contact: %{email: email} = contact}, _ -> + {:ok, "#{email}#{contact[:default_with_string]}"} + args, _ -> + {:error, "Got #{inspect args} instead"} + end + end + + field :something, + type: :string, + args: [ + name: [type: :input_name], + flag: [type: :boolean, default_value: false], + ], + resolve: fn + %{name: %{first_name: name}}, _ -> + {:ok, name} + %{flag: val}, _ -> + {:ok, @res[val]} + _, _ -> + {:error, "No value provided for flag argument"} + end + field :required_thing, :string do + arg :name, non_null(:input_name) + resolve fn + %{name: %{first_name: name}}, _ -> {:ok, name} + args, _ -> {:error, "Got #{inspect args} instead"} + end + end + + end + + end + + context "arguments with variables" do + it "should raise an error when a non null argument variable is null" do + doc = """ + query GetContacts($contacts:[ContactInput]){contacts(contacts:$contacts)} + """ + assert_result {:ok, %{errors: [%{message: ~s(In argument "contacts": Expected type "[ContactInput]!", found null.)}]}}, + doc |> run(Schema) + end + + context "list inputs" do + + it "works with basic scalars" do + doc = """ + query GetNumbers($numbers:[Int!]!){numbers(numbers:$numbers)} + """ + assert_result {:ok, %{data: o%{"numbers" => [1, 2]}}}, doc |> run(Schema, variables: %{"numbers" =>[1, 2]}) + end + + it "works with custom scalars" do + doc = """ + query GetNames($names:[Name!]!){names(names:$names)} + """ + assert_result {:ok, %{data: o%{"names" => ["Joe", "bob"]}}}, doc |> run(Schema, variables: %{"names" => ["Joe", "bob"]}) + end + + it "works with input objects" do + doc = """ + query GetContacts($contacts:[ContactInput]){contacts(contacts:$contacts)} + """ + assert_result {:ok, %{data: o%{"contacts" => ["a@b.com", "c@d.com"]}}}, doc |> run(Schema, variables: %{"contacts" => [%{"email" => "a@b.com"}, %{"email" => "c@d.com"}]}) + end + end + + context "input object arguments" do + it "works in a basic case" do + doc = """ + query FindUser($contact: ContactInput!){ + user(contact:$contact) + } + """ + assert_result {:ok, %{data: o%{"user" => "bubba@joe.comasdf"}}}, doc |> run(Schema, variables: %{"contact" => %{"email" => "bubba@joe.com"}}) + end + end + + context "custom scalar arguments" do + it "works when specified as non null" do + doc = """ + { requiredThing(name: "bob") } + """ + assert_result {:ok, %{data: o%{"requiredThing" => "bob"}}}, doc |> run(Schema) + end + + it "works when passed to resolution" do + assert_result {:ok, %{data: o%{"something" => "bob"}}}, "{ something(name: \"bob\") }" |> run(Schema) + end + end + + context "boolean arguments" do + + it "are passed as arguments to resolution functions correctly" do + doc = """ + query DoSomething($flag: Boolean!) { + something(flag:$flag) + } + """ + assert_result {:ok, %{data: o%{"something" => "YES"}}}, doc |> run(Schema, variables: %{"flag" => true}) + assert_result {:ok, %{data: o%{"something" => "NO"}}}, doc |> run(Schema, variables: %{"flag" => false}) + end + + it "If a variable is not provided schema default value is used" do + doc = """ + query DoSomething($flag: Boolean) { + something(flag: $flag) + } + """ + assert_result {:ok, %{data: o%{"something" => "NO"}}}, doc |> Absinthe.run(Schema, variables: %{}) + end + + it "works with input objects with inner variables" do + doc = """ + query Blah($email: String){contacts(contacts: [{email: $email}, {email: $email}])} + """ + assert_result {:ok, %{data: o%{"contacts" => ["a@b.com", "a@b.com"]}}}, doc |> run(Schema, variables: %{"email" => "a@b.com"}) + end + + it "enforces non_null fields in input passed as variable" do + query = """ + query Stuff($input: InputStuff!) { + stuff(stuff: $input) + } + """ + result = run(query, Schema, variables: %{"input" => %{"value" => 5, "nonNullField" => nil}}) + assert_result {:ok, %{errors: [%{message: ~s(Argument "stuff" has invalid value $input.\nIn field "nonNullField": Expected type "String!", found null.)}]}}, result + + result = run(query, Schema, variables: %{"input" => %{"value" => 5}}) + assert_result {:ok, %{errors: [%{message: ~s(Argument "stuff" has invalid value $input.\nIn field "nonNullField": Expected type "String!", found null.)}]}}, result + end + + it "can set input object default values" do + doc = """ + query FooIsMissing($email: String, $defaultWithString: String) { + user(contact: {email: $email, defaultWithString: $defaultWithString}) + } + """ + assert_result {:ok, %{data: o%{"user" => "bubba@joe.comasdf"}}}, doc |> run(Schema, variables: %{"email" => "bubba@joe.com"}) + end + + it "works with input objects with inner variables when no variables are given" do + doc = """ + query Blah($email: String){contacts(contacts: [{email: $email}, {email: $email}])} + """ + assert_result {:ok, %{errors: [%{message: "Argument \"contacts\" has invalid value [{email: $email}, {email: $email}].\nIn element #1: Expected type \"ContactInput\", found {email: $email}.\nIn field \"email\": Expected type \"String!\", found $email.\nIn element #2: Expected type \"ContactInput\", found {email: $email}.\nIn field \"email\": Expected type \"String!\", found $email."}]}}, doc |> run(Schema, variables: %{}) + end + + it "works with lists with inner variables" do + doc = """ + query Blah($contact: ContactInput){contacts(contacts: [$contact, $contact])} + """ + assert_result {:ok, %{data: o%{"contacts" => ["a@b.com", "a@b.com"]}}}, doc |> run(Schema, variables: %{"contact" => %{"email" => "a@b.com"}}) + end + + it "works with lists with inner variables when no variables are given" do + doc = """ + query Blah($contact: ContactInput){contacts(contacts: [$contact, $contact])} + """ + assert_result {:ok, %{data: o%{"contacts" => []}}}, doc |> run(Schema, variables: %{}) + end + + end + + context "nullable arguments" do + it "if omitted should still be passed as an argument map to the resolver" do + doc = """ + query GetContact{ contact } + """ + assert_result {:ok, %{data: o%{"contact" => nil}}}, doc |> run(Schema) + end + end + + context "enum types" do + it "should work with valid values" do + doc = """ + query GetContact($type:ContactType){ contact(type: $type) } + """ + assert_result {:ok, %{data: o%{"contact" => "Email"}}}, doc |> run(Schema, variables: %{"type" => "Email"}) + end + + it "should work when nested" do + doc = """ + query FindUser($contact: ContactInput!){ + user(contact:$contact) + } + """ + assert_result {:ok, %{data: o%{"user" => "bubba@joe.comasdf"}}}, doc |> run(Schema, variables: %{"contact" => %{"email" => "bubba@joe.com", "contactType" => "Email"}}) + end + + it "should return an error with invalid values" do + assert_result {:ok, %{errors: [%{message: ~s(Argument "type" has invalid value "bagel".)}]}}, + "{ contact(type: \"bagel\") }" |> run(Schema) + end + + end + end + + context "literal arguments" do + context "missing arguments" do + it "returns the appropriate error" do + doc = """ + { requiredThing } + """ + assert_result {:ok, %{errors: [%{message: ~s(In argument "name": Expected type "InputName!", found null.)}]}}, doc |> run(Schema) + end + end + + context "list inputs" do + it "works with basic scalars" do + doc = """ + {numbers(numbers: [1, 2])} + """ + assert_result {:ok, %{data: o%{"numbers" => [1, 2]}}}, doc |> run(Schema) + end + + it "works for nested lists" do + doc = """ + { + listOfLists(items: [["foo"], ["bar", "baz"]]) + } + """ + assert_result {:ok, %{data: o%{"listOfLists" => [["foo"], ["bar", "baz"]]}}}, doc |> run(Schema) + end + + it "it will coerce a non list item if it's of the right type" do + # per https://facebook.github.io/graphql/#sec-Lists + doc = """ + {numbers(numbers: 1)} + """ + assert_result {:ok, %{data: o%{"numbers" => [1]}}}, doc |> run(Schema) + end + + it "works with custom scalars" do + doc = """ + {names(names: ["Joe", "bob"])} + """ + assert_result {:ok, %{data: o%{"names" => ["Joe", "bob"]}}}, doc |> run(Schema) + end + + it "works with input objects" do + doc = """ + {contacts(contacts: [{email: "a@b.com"}, {email: "c@d.com"}])} + """ + assert_result {:ok, %{data: o%{"contacts" => ["a@b.com", "c@d.com"]}}}, doc |> run(Schema) + end + + it "returns deeply nested errors" do + doc = """ + {contacts(contacts: [{email: "a@b.com"}, {foo: "c@d.com"}])} + """ + assert_result {:ok, %{errors: [ + %{message: "Argument \"contacts\" has invalid value [{email: \"a@b.com\"}, {foo: \"c@d.com\"}].\nIn element #2: Expected type \"ContactInput\", found {foo: \"c@d.com\"}.\nIn field \"email\": Expected type \"String!\", found null.\nIn field \"foo\": Unknown field."}, + ]}}, + doc |> run(Schema) + end + end + + context "input object arguments" do + it "works in a basic case" do + doc = """ + {user(contact: {email: "bubba@joe.com"})} + """ + assert_result {:ok, %{data: o%{"user" => "bubba@joe.comasdf"}}}, doc |> run(Schema) + end + + it "works with inner booleans set to false" do + # This makes sure we don't accidentally filter out booleans when trying + # to filter out nils + doc = """ + {testBooleanInputObject(input: {flag: false})} + """ + assert_result {:ok, %{data: o%{"testBooleanInputObject" => false}}}, doc |> run(Schema) + end + + it "works in a nested case" do + doc = """ + {user(contact: {email: "bubba@joe.com", nestedContactInput: {email: "foo"}})} + """ + assert_result {:ok, %{data: o%{"user" => "bubba@joe.comasdf"}}}, doc |> run(Schema) + end + + it "returns the correct error if an inner field is marked non null but is missing" do + doc = """ + {user(contact: {foo: "buz"})} + """ + assert_result {:ok, %{errors: [ + %{message: ~s(Argument "contact" has invalid value {foo: "buz"}.\nIn field "email": Expected type "String!", found null.\nIn field "foo": Unknown field.)}, + ]}}, + doc |> run(Schema) + end + + it "returns an error if extra fields are given" do + doc = """ + {user(contact: {email: "bubba", foo: "buz"})} + """ + assert_result {:ok, %{errors: [%{message: "Argument \"contact\" has invalid value {email: \"bubba\", foo: \"buz\"}.\nIn field \"foo\": Unknown field."}]}}, + doc |> run(Schema) + end + end + + context "custom scalar arguments" do + it "works when specified as non null" do + doc = """ + { requiredThing(name: "bob") } + """ + assert_result {:ok, %{data: o%{"requiredThing" => "bob"}}}, doc |> run(Schema) + end + it "works when passed to resolution" do + assert_result {:ok, %{data: o%{"something" => "bob"}}}, "{ something(name: \"bob\") }" |> run(Schema) + end + end + + context "boolean arguments" do + + it "are passed as arguments to resolution functions correctly" do + assert_result {:ok, %{data: o%{"something" => "YES"}}}, "{ something(flag: true) }" |> run(Schema) + assert_result {:ok, %{data: o%{"something" => "NO"}}}, "{ something(flag: false) }" |> run(Schema) + assert_result {:ok, %{data: o%{"something" => "NO"}}}, "{ something }" |> run(Schema) + end + + it "returns a correct error when passed the wrong type" do + assert_result {:ok, %{errors: [%{message: ~s(Argument "flag" has invalid value {foo: 1}.\nIn field \"foo\": Unknown field.)}]}}, + "{ something(flag: {foo: 1}) }" |> run(Schema) + end + end + + context "enum types" do + it "should work with valid values" do + assert_result {:ok, %{data: o%{"contact" => "Email"}}}, "{ contact(type: Email) }" |> run(Schema) + end + it "should return an error with invalid values" do + assert_result {:ok, %{errors: [%{message: ~s(Argument "type" has invalid value "bagel".)}]}}, + "{ contact(type: \"bagel\") }" |> run(Schema) + end + end + end + + context "camelized errors" do + it "should adapt internal field names on error" do + doc = """ + query FindUser { + user(contact: {email: "bubba@joe.com", contactType: 1}) + } + """ + assert_result {:ok, %{errors: [%{message: ~s(Argument "contact" has invalid value {email: "bubba@joe.com", contactType: 1}.\nIn field "contactType": Expected type "ContactType", found 1.)}]}}, run(doc, Schema) + end + end + +end diff --git a/test/lib/absinthe/execution/ordered_default_resolver_test.exs b/test/lib/absinthe/execution/ordered_default_resolver_test.exs new file mode 100644 index 0000000000..71a75ecfe3 --- /dev/null +++ b/test/lib/absinthe/execution/ordered_default_resolver_test.exs @@ -0,0 +1,59 @@ +defmodule Absinthe.Execution.OrderedDefaultResolverTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + @root %{:foo => "baz", "bar" => "quux"} + @query "{ foo bar }" + + context "without a custom default resolver defined" do + + defmodule NormalSchema do + use Absinthe.Schema + + query do + field :foo, :string + field :bar, :string + end + + end + + it "should resolve using atoms" do + assert {:ok, %{data: o%{"foo" => "baz", "bar" => nil}}} == Absinthe.run(@query, NormalSchema, root_value: @root) + end + + end + + context "with a custom default resolver defined" do + + defmodule CustomSchema do + use Absinthe.Schema + + query do + field :foo, :string + field :bar, :string + end + + def middleware(middleware, %{name: name, identifier: identifier} = field, obj) do + middleware_spec = Absinthe.Resolution.resolver_spec(fn parent, _, _ -> + case parent do + %{^name => value} -> {:ok, value} + %{^identifier => value} -> {:ok, value} + _ -> {:ok, nil} + end + end) + + Absinthe.Schema.replace_default(middleware, middleware_spec, field, obj) + end + def middleware(middleware, _, _) do + middleware + end + + end + + it "should resolve using as defined" do + assert {:ok, %{data: o%{"foo" => "baz", "bar" => "quux"}}} == Absinthe.run(@query, CustomSchema, root_value: @root) + end + + end + +end diff --git a/test/lib/absinthe/execution/ordered_fragment_spread_test.exs b/test/lib/absinthe/execution/ordered_fragment_spread_test.exs new file mode 100644 index 0000000000..663d2fadf1 --- /dev/null +++ b/test/lib/absinthe/execution/ordered_fragment_spread_test.exs @@ -0,0 +1,48 @@ +defmodule Absinthe.Execution.OrderedFragmentSpreadTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + @query """ + query AbstractFragmentSpread { + firstSearchResult { + ...F0 + } + } + + fragment F0 on SearchResult { + __typename + ...F1 + } + + fragment F1 on Person { + age + } + """ + + it "spreads fragments with abstract target" do + assert {:ok, %{data: o%{"firstSearchResult" => o%{"__typename" => "Person", "age" => 35}}}} == Absinthe.run(@query, ContactSchema) + end + + it "spreads errors fragments that don't refer to a real type" do + query = """ + query { + __typename + } + fragment F0 on Foo { + name + } + """ + assert {:ok, %{errors: [%{locations: [%{column: 0, line: 4}], message: "Unknown type \"Foo\"."}]}} == Absinthe.run(query, ContactSchema) + end + + it "errors properly when spreading fragments that don't exist" do + query = """ + query { + __typename + ... NonExistentFragment + } + """ + assert {:ok, %{errors: [%{locations: [%{column: 0, line: 3}], message: "Unknown fragment \"NonExistentFragment\""}]}} == Absinthe.run(query, ContactSchema) + end + +end diff --git a/test/lib/absinthe/execution/ordered_inline_fragments_test.exs b/test/lib/absinthe/execution/ordered_inline_fragments_test.exs new file mode 100644 index 0000000000..6f7d18241c --- /dev/null +++ b/test/lib/absinthe/execution/ordered_inline_fragments_test.exs @@ -0,0 +1,43 @@ +defmodule Absinthe.Execution.OrderedInlineFragmentsTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + @query """ + { + person { + name + ... on Person { + age + } + } + } + """ + + it "adds fields in a simple case" do + assert {:ok, %{data: o%{"person" => o%{"name" => "Bruce", "age" => 35}}}} == Absinthe.run(@query, ContactSchema) + end + + @query """ + query Q($business: Boolean = false) { + contact(business: $business) { + entity { + name + ... on Person { + age + } + ... on Business { + employeeCount + } + } + } + } + """ + + it "adds fields in an interface query based on a type" do + assert {:ok, %{data: o%{"contact" => o%{"entity" => o%{"name" => "Bruce", "age" => 35}}}}} == run(@query, ContactSchema, variables: %{"business" => false}) + end + it "adds fields in an interface query based on another type" do + assert {:ok, %{data: o%{"contact" => o%{"entity" => o%{"name" => "Someplace", "employeeCount" => 11}}}}} == run(@query, ContactSchema, variables: %{"business" => true}) + end + +end diff --git a/test/lib/absinthe/execution/ordered_list_test.exs b/test/lib/absinthe/execution/ordered_list_test.exs new file mode 100644 index 0000000000..536a2693ec --- /dev/null +++ b/test/lib/absinthe/execution/ordered_list_test.exs @@ -0,0 +1,183 @@ +defmodule Absinthe.Execution.OrderedListTest.Schema do + use Absinthe.Schema + + object :item do + field :categories, list_of(:string) + end + + object :book do + field :name, :string + end + + query do + field :numbers, list_of(:integer), resolve: fn _, _ -> {:ok, [1,2,3]} end + field :categories, list_of(:string) do + resolve fn _, _ -> + {:ok, ["foo", "bar", "baz"]} + end + end + + field :items, list_of(:item) do + resolve fn _, _ -> + items = [ + %{categories: ["foo", "bar"]}, + %{categories: ["baz", "buz"]}, + ] + {:ok, items} + end + end + + field :list_of_list_of_numbers, list_of(list_of(:integer)) do + resolve fn _, _ -> {:ok, [[1, 2, 3], [4, 5, 6]]} end + end + + field :big_nesting_of_numbers, list_of(list_of(list_of(list_of(:integer)))) do + resolve fn _, _ -> + list = [[ + [ + [1, 2, 3], [4, 5, 6] + ], + [ + [7, 8, 9] + ], + [ + [10, 11, 12] + ] + ]] + {:ok, list} + end + end + + field :list_of_list_of_books, list_of(list_of(:book)) do + resolve fn _, _ -> + books = [[ + %{name: "foo"}, + %{name: "bar"}, + ], [ + %{name: "baz"}, + ]] + {:ok, books} + end + end + + field :list_of_list_of_items, list_of(list_of(:item)) do + resolve fn _, _ -> + items = [[ + %{categories: ["foo", "bar"]}, + %{categories: ["baz", "buz"]}, + ], [ + %{categories: ["blip", "blop"]}, + ]] + {:ok, items} + end + end + end +end + +defmodule Absinthe.Execution.OrderedListTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + @query """ + { + categories + } + """ + + it "should resolve list of strings" do + assert {:ok, %{data: o(%{"categories" => ["foo", "bar", "baz"]})}} == + Absinthe.run(@query, __MODULE__.Schema) + end + + @query """ + { + numbers + } + """ + + it "should resolve list of numbers" do + assert {:ok, %{data: o%{"numbers" => [1,2,3]}}} == + Absinthe.run(@query, __MODULE__.Schema) + end + + @query """ + { + items { + categories + } + } + """ + + it "should resolve list of objects with a list of scalars inside" do + assert {:ok, %{data: o%{"items" => [o(%{"categories" => ["foo", "bar"]}), o%{"categories" => ["baz", "buz"]}]}}} == + Absinthe.run(@query, __MODULE__.Schema) + end + + @query """ + { + listOfListOfNumbers + } + """ + it "should resolve list of list of numbers" do + assert {:ok, %{data: o%{"listOfListOfNumbers" => [[1,2,3],[4,5,6]]}}} == + Absinthe.run(@query, __MODULE__.Schema) + end + + @query """ + { + bigNestingOfNumbers + } + """ + it "should resolve list of lists of... numbers with a depth of 4" do + list = [[ + [ + [1, 2, 3], [4, 5, 6] + ], + [ + [7, 8, 9] + ], + [ + [10, 11, 12] + ] + ]] + assert {:ok, %{data: o%{"bigNestingOfNumbers" => list}}} == + Absinthe.run(@query, __MODULE__.Schema) + end + + @query """ + { + listOfListOfBooks { + name + } + } + """ + it "should resolve list of list of books" do + books = [[ + o(%{"name" => "foo"}), + o(%{"name" => "bar"}), + ], [ + o%{"name" => "baz"} + ]] + assert {:ok, %{data: o%{"listOfListOfBooks" => books}}} == + Absinthe.run(@query, __MODULE__.Schema) + end + + @query """ + { + listOfListOfItems { + categories + } + } + """ + it "should resolve list of list of items" do + items = [[ + o(%{"categories" => ["foo", "bar"]}), + o%{"categories" => ["baz", "buz"]} + ], [ + o%{"categories" => ["blip", "blop"]} + ]] + assert {:ok, %{data: o%{"listOfListOfItems" => items}}} == + Absinthe.run(@query, __MODULE__.Schema) + end + +end diff --git a/test/lib/absinthe/execution/ordered_subscription_test.exs b/test/lib/absinthe/execution/ordered_subscription_test.exs new file mode 100644 index 0000000000..a598c3bbf7 --- /dev/null +++ b/test/lib/absinthe/execution/ordered_subscription_test.exs @@ -0,0 +1,223 @@ +defmodule Absinthe.Execution.OrderedSubscriptionTest do + use Absinthe.Case, async: false, ordered: true, import_run: false + use OrdMap + + import ExUnit.CaptureLog + + defmodule PubSub do + @behaviour Absinthe.Subscription.Pubsub + + def start_link() do + Registry.start_link(:unique, __MODULE__) + end + + def subscribe(topic) do + Registry.register(__MODULE__, topic, []) + :ok + end + + def publish_subscription(topic, data) do + message = %{ + topic: topic, + event: "subscription:data", + result: data, + } + + Registry.dispatch(__MODULE__, topic, fn entries -> + for {pid, _} <- entries, do: send(pid, {:broadcast, message}) + end) + end + + def publish_mutation(_proxy_topic, _mutation_result, _subscribed_fields) do + # this pubsub is local and doesn't support clusters + :ok + end + end + + defmodule Schema do + use Absinthe.Schema + + query do + #Query type must exist + end + + object :user do + field :id, :id + field :name, :string + field :group, :group do + resolve fn user, _, %{context: %{test_pid: pid}} -> + batch({__MODULE__, :batch_get_group, pid}, nil, fn _results -> + {:ok, user.group} + end) + end + end + end + + object :group do + field :name, :string + end + + def batch_get_group(test_pid, _) do + # send a message to the test process every time we access this function. + # if batching is working properly, it should only happen once. + send(test_pid, :batch_get_group) + %{} + end + + subscription do + field :raises, :string do + config fn _, _ -> + {:ok, topic: "*"} + end + resolve fn _, _, _ -> + raise "boom" + end + end + + field :user, :user do + arg :id, :id + config fn args, _ -> + {:ok, topic: args[:id] || "*"} + end + end + + field :thing, :string do + arg :client_id, non_null(:id) + + config fn + _args, %{context: %{authorized: false}} -> + {:error, "unauthorized"} + args, _ -> + { + :ok, + topic: args.client_id, + } + end + + end + end + + end + + setup_all do + {:ok, _} = PubSub.start_link() + {:ok, _} = Absinthe.Subscription.start_link(PubSub) + :ok + end + + @query """ + subscription ($clientId: ID!) { + thing(clientId: $clientId) + } + """ + test "can subscribe the current process" do + client_id = "abc" + assert {:ok, %{"subscribed" => topic}} = run(@query, Schema, variables: %{"clientId" => client_id}, context: %{pubsub: PubSub}) + + Absinthe.Subscription.publish(PubSub, "foo", thing: client_id) + + assert_receive({:broadcast, msg}) + + assert %{ + event: "subscription:data", + result: %{data: o%{"thing" => "foo"}}, + topic: topic + } == msg + end + + @query """ + subscription ($clientId: ID!) { + thing(clientId: $clientId, extra: 1) + } + """ + test "can return errors properly" do + assert { + :ok, + %{errors: [%{locations: [%{column: 0, line: 2}], + message: "Unknown argument \"extra\" on field \"thing\" of type \"RootSubscriptionType\"."}]} + } == run(@query, Schema, variables: %{"clientId" => "abc"}, context: %{pubsub: PubSub}) + end + + @query """ + subscription ($clientId: ID!) { + thing(clientId: $clientId) + } + """ + test "can return an error tuple from the topic function" do + assert {:ok, %{errors: [%{locations: [%{column: 0, line: 2}], message: "unauthorized"}]}} + == run(@query, Schema, variables: %{"clientId" => "abc"}, context: %{pubsub: PubSub, authorized: false}) + end + + @query """ + subscription ($clientId: ID!) { + thing(clientId: $clientId) + } + """ + test "stringifies topics" do + assert {:ok, %{"subscribed" => topic}} = run(@query, Schema, variables: %{"clientId" => "1"}, context: %{pubsub: PubSub}) + + Absinthe.Subscription.publish(PubSub, "foo", thing: 1) + + assert_receive({:broadcast, msg}) + + assert %{ + event: "subscription:data", + result: %{data: o%{"thing" => "foo"}}, + topic: topic + } == msg + end + + + test "isn't tripped up if one of the subscription docs raises" do + assert {:ok, %{"subscribed" => _}} = run("subscription { raises }", Schema) + assert {:ok, %{"subscribed" => topic}} = run("subscription { thing(clientId: \"*\")}", Schema) + + error_log = capture_log(fn -> + Absinthe.Subscription.publish(PubSub, "foo", raises: "*", thing: "*") + + assert_receive({:broadcast, msg}) + + assert %{ + event: "subscription:data", + result: %{data: o%{"thing" => "foo"}}, + topic: topic + } == msg + end) + + assert String.contains?(error_log, "boom") + end + + test "different subscription docs are batched together" do + opts = [context: %{test_pid: self()}] + assert {:ok, %{"subscribed" => doc1}} = run("subscription { user { group { name } id} }", Schema, opts) + # different docs required for test, otherwise they get deduplicated from the start + assert {:ok, %{"subscribed" => doc2}} = run("subscription { user { group { name } id name} }", Schema, opts) + + user = %{id: "1", name: "Alicia", group: %{name: "Elixir Users"}} + + Absinthe.Subscription.publish(PubSub, user, user: ["*", user.id]) + + assert_receive({:broadcast, %{topic: ^doc1, result: %{data: _}}}) + assert_receive({:broadcast, %{topic: ^doc2, result: result}}) + + name = result.data + |> OrdMap.get("user") + |> OrdMap.get("group") + |> OrdMap.get("name") + assert name == "Elixir Users" + + # we should get this just once due to batching + assert_receive(:batch_get_group) + refute_receive(:batch_get_group) + end + + defp run(query, schema, opts \\ []) do + opts = Keyword.update(opts, :context, %{pubsub: PubSub}, &Map.put(&1, :pubsub, PubSub)) + case Absinthe.run(query, schema, opts) do + {:ok, %{"subscribed" => topic}} = val -> + PubSub.subscribe(topic) + val + val -> val + end + end +end diff --git a/test/lib/absinthe/execution/subscription_test.exs b/test/lib/absinthe/execution/subscription_test.exs index 84064123c7..793c9996de 100644 --- a/test/lib/absinthe/execution/subscription_test.exs +++ b/test/lib/absinthe/execution/subscription_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.Execution.SubscriptionTest do - use ExUnit.Case + use Absinthe.Case, async: false, ordered: false, import_run: false import ExUnit.CaptureLog @@ -215,4 +215,4 @@ defmodule Absinthe.Execution.SubscriptionTest do val -> val end end -end +end \ No newline at end of file diff --git a/test/lib/absinthe/extensions_test.exs b/test/lib/absinthe/extensions_test.exs index b9963e3be9..73803c883e 100644 --- a/test/lib/absinthe/extensions_test.exs +++ b/test/lib/absinthe/extensions_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.ExtensionsTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false defmodule Schema do use Absinthe.Schema @@ -38,7 +38,7 @@ defmodule Absinthe.ExtensionsTest do pipeline = Schema |> Absinthe.Pipeline.for_document() - |> Absinthe.Pipeline.insert_after(Absinthe.Phase.Document.Result, MyPhase) + |> Absinthe.Pipeline.insert_after(Absinthe.Utils.getDefaultDocumentResult(), MyPhase) assert {:ok, bp, _} = Absinthe.Pipeline.run(doc, pipeline) diff --git a/test/lib/absinthe/fragment_merge_test.exs b/test/lib/absinthe/fragment_merge_test.exs index 59dbf9d022..ef2419f506 100644 --- a/test/lib/absinthe/fragment_merge_test.exs +++ b/test/lib/absinthe/fragment_merge_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.FragmentMergeTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false defmodule Schema do use Absinthe.Schema diff --git a/test/lib/absinthe/introspection_test.exs b/test/lib/absinthe/introspection_test.exs index 17a87cb4e9..2186ca171b 100644 --- a/test/lib/absinthe/introspection_test.exs +++ b/test/lib/absinthe/introspection_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.IntrospectionTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false import AssertResult alias Absinthe.Schema diff --git a/test/lib/absinthe/middleware/async_test.exs b/test/lib/absinthe/middleware/async_test.exs index 3a3aead300..a004698d49 100644 --- a/test/lib/absinthe/middleware/async_test.exs +++ b/test/lib/absinthe/middleware/async_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.Middleware.AsyncTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false defmodule Schema do use Absinthe.Schema diff --git a/test/lib/absinthe/middleware/batch_test.exs b/test/lib/absinthe/middleware/batch_test.exs index 4aadd706c8..3ad72987ce 100644 --- a/test/lib/absinthe/middleware/batch_test.exs +++ b/test/lib/absinthe/middleware/batch_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.Middleware.BatchTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false defmodule Schema do use Absinthe.Schema diff --git a/test/lib/absinthe/middleware/dataloader_test.exs b/test/lib/absinthe/middleware/dataloader_test.exs index 72dcfe7f9f..581fce3e43 100644 --- a/test/lib/absinthe/middleware/dataloader_test.exs +++ b/test/lib/absinthe/middleware/dataloader_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.Middleware.DataloaderTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false defmodule Schema do use Absinthe.Schema diff --git a/test/lib/absinthe/middleware/ordered_async_test.exs b/test/lib/absinthe/middleware/ordered_async_test.exs new file mode 100644 index 0000000000..49688312e7 --- /dev/null +++ b/test/lib/absinthe/middleware/ordered_async_test.exs @@ -0,0 +1,63 @@ +defmodule Absinthe.Middleware.OrderedAsyncTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + defmodule Schema do + use Absinthe.Schema + + query do + field :async_thing, :string do + resolve fn _, _, _ -> + async(fn -> + async(fn -> + {:ok, "we async now"} + end) + end) + end + end + + field :other_async_thing, :string do + resolve cool_async fn _, _, _ -> + {:ok, "magic"} + end + end + + field :returns_nil, :string do + resolve cool_async fn _, _, _ -> + {:ok, nil} + end + end + end + + def cool_async(fun) do + fn _source, _args, _info -> + async(fn -> + {:middleware, Absinthe.Resolution, fun} + end) + end + end + + end + + it "can resolve a field using the normal async helper" do + doc = """ + {asyncThing} + """ + assert {:ok, %{data: o%{"asyncThing" => "we async now"}}} == Absinthe.run(doc, Schema) + end + + it "can resolve a field using a cooler but probably confusing to some people helper" do + doc = """ + {otherAsyncThing} + """ + assert {:ok, %{data: o%{"otherAsyncThing" => "magic"}}} == Absinthe.run(doc, Schema) + end + + it "can return nil from an async field safely" do + doc = """ + {returnsNil} + """ + assert {:ok, %{data: o%{"returnsNil" => nil}}} == Absinthe.run(doc, Schema) + end + +end diff --git a/test/lib/absinthe/middleware/ordered_batch_test.exs b/test/lib/absinthe/middleware/ordered_batch_test.exs new file mode 100644 index 0000000000..19e126560a --- /dev/null +++ b/test/lib/absinthe/middleware/ordered_batch_test.exs @@ -0,0 +1,100 @@ +defmodule Absinthe.Middleware.OrderedBatchTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + defmodule Schema do + use Absinthe.Schema + + @organizations 1..3 |> Map.new(&{&1, %{ + id: &1, + name: "Organization: ##{&1}" + }}) + @users 1..3 |> Enum.map(&%{ + id: &1, + name: "User: ##{&1}", + organization_id: &1, + }) + + object :organization do + field :id, :integer + field :name, :string + end + + object :user do + field :name, :string + field :organization, :organization do + resolve fn user, _, _ -> + batch({__MODULE__, :by_id}, user.organization_id, fn batch -> + {:ok, Map.get(batch, user.organization_id)} + end) + end + end + end + + query do + field :users, list_of(:user) do + resolve fn _, _, _ -> {:ok, @users} end + end + field :organization, :organization do + arg :id, non_null(:integer) + resolve fn _, %{id: id}, _ -> + batch({__MODULE__, :by_id}, id, fn batch -> + {:ok, Map.get(batch, id)} + end) + end + end + end + + def by_id(_, ids) do + Map.take(@organizations, ids) + end + end + + it "can resolve a field using the normal async helper" do + doc = """ + { + users { + organization { + name + } + } + } + """ + expected_data = o%{"users" => [ + o(%{"organization" => o%{"name" => "Organization: #1"}}), + o(%{"organization" => o%{"name" => "Organization: #2"}}), + o%{"organization" => o%{"name" => "Organization: #3"}} + ]} + + assert {:ok, %{data: data}} = Absinthe.run(doc, Schema) + assert expected_data == data + end + + + it "can resolve batched fields cross-query that have different data requirements" do + doc = """ + { + users { + organization { + name + } + } + organization(id: 1) { + id + } + } + """ + expected_data = o%{ + "users" => [ + o(%{"organization" => o%{"name" => "Organization: #1"}}), + o(%{"organization" => o%{"name" => "Organization: #2"}}), + o%{"organization" => o%{"name" => "Organization: #3"}} + ], + "organization" => o%{"id" => 1} + } + + assert {:ok, %{data: data}} = Absinthe.run(doc, Schema) + assert expected_data == data + end + +end diff --git a/test/lib/absinthe/middleware/ordered_dataloader_test.exs b/test/lib/absinthe/middleware/ordered_dataloader_test.exs new file mode 100644 index 0000000000..c4a52df28c --- /dev/null +++ b/test/lib/absinthe/middleware/ordered_dataloader_test.exs @@ -0,0 +1,126 @@ +defmodule Absinthe.Middleware.OrderedDataloaderTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + defmodule Schema do + use Absinthe.Schema + + import Absinthe.Resolution.Helpers + + @organizations 1..3 |> Map.new(&{&1, %{ + id: &1, + name: "Organization: ##{&1}" + }}) + @users 1..3 |> Enum.map(&%{ + id: &1, + name: "User: ##{&1}", + organization_id: &1, + }) + + defp batch_load({:organization, test_pid}, ids) do + send test_pid, :loading + Map.take(@organizations, ids) + end + + def context(ctx) do + source = Dataloader.KV.new(&batch_load/2) + loader = Dataloader.add_source(Dataloader.new, :test, source) + + Map.merge(ctx, %{ + loader: loader, + test_pid: self() + }) + end + + def plugins do + [Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults() + end + + object :organization do + field :id, :integer + field :name, :string + end + + object :user do + field :name, :string + field :organization, :organization do + resolve fn user, _, %{context: %{loader: loader, test_pid: test_pid}} -> + loader + |> Dataloader.load(:test, {:organization, test_pid}, user.organization_id) + |> on_load(fn loader -> + {:ok, Dataloader.get(loader, :test, {:organization, test_pid}, user.organization_id)} + end) + end + end + end + + query do + field :users, list_of(:user) do + resolve fn _, _, _ -> {:ok, @users} end + end + field :organization, :organization do + arg :id, non_null(:integer) + resolve fn _, %{id: id}, %{context: %{loader: loader, test_pid: test_pid}} -> + loader + |> Dataloader.load(:test, {:organization, test_pid}, id) + |> on_load(fn loader -> + {:ok, Dataloader.get(loader, :test, {:organization, test_pid}, id)} + end) + end + end + end + + end + + it "can resolve a field using the normal dataloader helper" do + doc = """ + { + users { + organization { + name + } + } + } + """ + expected_data = o(%{"users" => [ + o(%{"organization" => o(%{"name" => "Organization: #1"})}), + o(%{"organization" => o(%{"name" => "Organization: #2"})}), + o(%{"organization" => o(%{"name" => "Organization: #3"})}) + ]}) + + assert {:ok, %{data: data}} = Absinthe.run(doc, Schema) + assert expected_data == data + + assert_receive(:loading) + refute_receive(:loading) + end + + it "can resolve batched fields cross-query that have different data requirements" do + doc = """ + { + users { + organization { + name + } + } + organization(id: 1) { + id + } + } + """ + expected_data = o%{ + "users" => [ + o(%{"organization" => o%{"name" => "Organization: #1"}}), + o(%{"organization" => o%{"name" => "Organization: #2"}}), + o%{"organization" => o%{"name" => "Organization: #3"}} + ], + "organization" => o%{"id" => 1} + } + + assert {:ok, %{data: data}} = Absinthe.run(doc, Schema) + assert expected_data == data + assert_receive(:loading) + refute_receive(:loading) + end + +end diff --git a/test/lib/absinthe/ordered_alias_test.exs b/test/lib/absinthe/ordered_alias_test.exs new file mode 100644 index 0000000000..cf6fc914ae --- /dev/null +++ b/test/lib/absinthe/ordered_alias_test.exs @@ -0,0 +1,40 @@ +defmodule Absinthe.Middleware.OrderedAliasTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + defmodule Schema do + use Absinthe.Schema + + object :user do + field :id, :integer + field :name, :string + end + + query do + field :user, :user do + resolve fn _, _, _ -> {:ok, %{id: 42, name: "foobar"}} end + end + end + end + + it "can resolve different sub-fields on aliased fields" do + doc = """ + { + userId : user { + id + } + userName : user { + name + } + } + """ + expected_data = o%{ + "userId" => o(%{"id" => 42}), + "userName" => o%{"name" => "foobar"} + } + + assert {:ok, %{data: data}} = Absinthe.run(doc, Schema) + assert expected_data == data + end + +end diff --git a/test/lib/absinthe/ordered_custom_types_test.exs b/test/lib/absinthe/ordered_custom_types_test.exs new file mode 100644 index 0000000000..98378b2cc1 --- /dev/null +++ b/test/lib/absinthe/ordered_custom_types_test.exs @@ -0,0 +1,290 @@ +defmodule Absinthe.OrderedCustomTypesTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + import AssertResult + + defmodule Schema do + use Absinthe.Schema + + import_types Absinthe.Type.Custom + + @custom_types %{ + datetime: %DateTime{ + year: 2017, month: 1, day: 27, + hour: 20, minute: 31, second: 55, + time_zone: "Etc/UTC", zone_abbr: "UTC", utc_offset: 0, std_offset: 0, + }, + naive_datetime: ~N[2017-01-27 20:31:55], + date: ~D[2017-01-27], + time: ~T[20:31:55], + decimal: Decimal.new("-3.49"), + } + + query do + field :custom_types_query, :custom_types_object do + resolve fn _, _ -> {:ok, @custom_types} end + end + end + + mutation do + field :custom_types_mutation, :result do + arg :args, :custom_types_input + resolve fn _, _ -> {:ok, %{message: "ok"}} end + end + end + + object :custom_types_object do + field :datetime, :datetime + field :naive_datetime, :naive_datetime + field :date, :date + field :time, :time + field :decimal, :decimal + end + + object :result do + field :message, :string + end + + input_object :custom_types_input do + field :datetime, :datetime + field :naive_datetime, :naive_datetime + field :date, :date + field :time, :time + field :decimal, :decimal + end + end + + context "custom datetime type" do + it "can use datetime type in queries" do + result = "{ custom_types_query { datetime } }" |> run(Schema) + assert_result {:ok, %{data: o%{"custom_types_query" => + o%{"datetime" => "2017-01-27T20:31:55Z"}}}}, result + end + it "can use datetime type in input_object" do + request = """ + mutation { + custom_types_mutation(args: { datetime: "2017-01-27T20:31:55Z" }) { + message + } + } + """ + result = run(request, Schema) + assert_result {:ok, %{data: o%{"custom_types_mutation" => + o%{"message" => "ok"}}}}, result + end + it "can use null in input_object" do + request = """ + mutation { + custom_types_mutation(args: { datetime: null }) { + message + } + } + """ + result = run(request, Schema) + assert_result {:ok, %{data: o%{"custom_types_mutation" => + o%{"message" => "ok"}}}}, result + end + it "returns an error when datetime value cannot be parsed" do + request = """ + mutation { + custom_types_mutation(args: { datetime: "abc" }) { + message + } + } + """ + assert {:ok, %{errors: _errors}} = run(request, Schema) + end + end + + context "custom naive datetime type" do + it "can use naive datetime type in queries" do + result = "{ custom_types_query { naive_datetime } }" |> run(Schema) + assert_result {:ok, %{data: o%{"custom_types_query" => + o%{"naive_datetime" => "2017-01-27T20:31:55"}}}}, result + end + it "can use naive datetime type in input_object" do + request = """ + mutation { + custom_types_mutation(args: { naive_datetime: "2017-01-27T20:31:55" }) { + message + } + } + """ + result = run(request, Schema) + assert_result {:ok, %{data: o%{"custom_types_mutation" => + o%{"message" => "ok"}}}}, result + end + it "can use null in input_object" do + request = """ + mutation { + custom_types_mutation(args: { naive_datetime: null }) { + message + } + } + """ + result = run(request, Schema) + assert_result {:ok, %{data: o%{"custom_types_mutation" => + o%{"message" => "ok"}}}}, result + end + it "returns an error when naive datetime value cannot be parsed" do + request = """ + mutation { + custom_types_mutation(args: { naive_datetime: "abc" }) { + message + } + } + """ + assert {:ok, %{errors: _errors}} = run(request, Schema) + end + end + + context "custom date type" do + it "can use date type in queries" do + result = "{ custom_types_query { date } }" |> run(Schema) + assert_result {:ok, %{data: o%{"custom_types_query" => + o%{"date" => "2017-01-27"}}}}, result + end + it "can use date type in input_object" do + request = """ + mutation { + custom_types_mutation(args: { date: "2017-01-27" }) { + message + } + } + """ + result = run(request, Schema) + assert_result {:ok, %{data: o%{"custom_types_mutation" => + o%{"message" => "ok"}}}}, result + end + it "can use null in input_object" do + request = """ + mutation { + custom_types_mutation(args: { date: null }) { + message + } + } + """ + result = run(request, Schema) + assert_result {:ok, %{data: o%{"custom_types_mutation" => + o%{"message" => "ok"}}}}, result + end + it "returns an error when date value cannot be parsed" do + request = """ + mutation { + custom_types_mutation(args: { date: "abc" }) { + message + } + } + """ + assert {:ok, %{errors: _errors}} = run(request, Schema) + end + end + + context "custom time type" do + it "can use time type in queries" do + result = "{ custom_types_query { time } }" |> run(Schema) + assert_result {:ok, %{data: o%{"custom_types_query" => + o%{"time" => "20:31:55"}}}}, result + end + it "can use time type in input_object" do + request = """ + mutation { + custom_types_mutation(args: { time: "20:31:55" }) { + message + } + } + """ + result = run(request, Schema) + assert_result {:ok, %{data: o%{"custom_types_mutation" => + o%{"message" => "ok"}}}}, result + end + it "can use null in input_object" do + request = """ + mutation { + custom_types_mutation(args: { time: null }) { + message + } + } + """ + result = run(request, Schema) + assert_result {:ok, %{data: o%{"custom_types_mutation" => + o%{"message" => "ok"}}}}, result + end + it "returns an error when time value cannot be parsed" do + request = """ + mutation { + custom_types_mutation(args: { time: "abc" }) { + message + } + } + """ + assert {:ok, %{errors: _errors}} = run(request, Schema) + end + end + + context "custom decimal type" do + it "can use decimal type in queries" do + result = "{ custom_types_query { decimal } }" |> run(Schema) + assert_result {:ok, %{data: o%{"custom_types_query" => + o%{"decimal" => "-3.49"}}}}, result + end + it "can use decimal type as string in input_object" do + request = """ + mutation { + custom_types_mutation(args: { decimal: "-3.49" }) { + message + } + } + """ + result = run(request, Schema) + assert_result {:ok, %{data: o%{"custom_types_mutation" => + o%{"message" => "ok"}}}}, result + end + it "can use decimal type as integer in input_object" do + request = """ + mutation { + custom_types_mutation(args: { decimal: 3 }) { + message + } + } + """ + result = run(request, Schema) + assert_result {:ok, %{data: o%{"custom_types_mutation" => + o%{"message" => "ok"}}}}, result + end + it "can use decimal type as float in input_object" do + request = """ + mutation { + custom_types_mutation(args: { decimal: -3.49 }) { + message + } + } + """ + result = run(request, Schema) + assert_result {:ok, %{data: o%{"custom_types_mutation" => + o%{"message" => "ok"}}}}, result + end + it "can use null in input_object" do + request = """ + mutation { + custom_types_mutation(args: { decimal: null }) { + message + } + } + """ + result = run(request, Schema) + assert_result {:ok, %{data: o%{"custom_types_mutation" => + o%{"message" => "ok"}}}}, result + end + it "returns an error when decimal value cannot be parsed" do + request = """ + mutation { + custom_types_mutation(args: { decimal: "abc" }) { + message + } + } + """ + assert {:ok, %{errors: _errors}} = run(request, Schema) + end + end +end diff --git a/test/lib/absinthe/ordered_extensions_test.exs b/test/lib/absinthe/ordered_extensions_test.exs new file mode 100644 index 0000000000..382b8afdb6 --- /dev/null +++ b/test/lib/absinthe/ordered_extensions_test.exs @@ -0,0 +1,48 @@ +defmodule Absinthe.OrderedExtensionsTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + defmodule Schema do + use Absinthe.Schema + + query do + field :foo, :string do + middleware :resolve_foo + end + end + + def resolve_foo(res, _opts) do + %{res | + value: "hello world", + state: :resolved, + extensions: %{foo: 1}, + } + end + end + + defmodule MyPhase do + # rolls up the extensions data into a top level result + def run(blueprint, _) do + extensions = get_ext(blueprint.execution.result.fields) + result = Map.put(blueprint.result, :extensions, extensions) + {:ok, %{blueprint | result: result}} + end + + defp get_ext([field]) do + field.extensions + end + end + + it "sets the extensions on the result properly" do + doc = "{foo}" + + pipeline = + Schema + |> Absinthe.Pipeline.for_document() + |> Absinthe.Pipeline.insert_after(Absinthe.Utils.getDefaultDocumentResult(), MyPhase) + + assert {:ok, bp, _} = Absinthe.Pipeline.run(doc, pipeline) + + assert bp.result == %{data: o(%{"foo" => "hello world"}), extensions: %{foo: 1}} + end +end diff --git a/test/lib/absinthe/ordered_fragment_merge_test.exs b/test/lib/absinthe/ordered_fragment_merge_test.exs new file mode 100644 index 0000000000..f316102975 --- /dev/null +++ b/test/lib/absinthe/ordered_fragment_merge_test.exs @@ -0,0 +1,113 @@ +defmodule Absinthe.OrderedFragmentMergeTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + defmodule Schema do + use Absinthe.Schema + + object :user do + field :todos, list_of(:todo) + end + + object :todo do + field :total_count, :integer + field :completed_count, :integer + end + + query do + field :viewer, :user do + resolve fn _, _ -> + {:ok, %{todos: [%{total_count: 1, completed_count: 2}, %{total_count: 3, completed_count: 4}]}} + end + end + end + end + + test "it deep merges fields properly" do + doc = """ + { + viewer { + ...fragmentWithOneField + ...fragmentWithOtherField + } + } + + fragment fragmentWithOneField on User { + todos { + totalCount + } + } + + fragment fragmentWithOtherField on User { + todos { + completedCount + } + } + """ + expected = o%{"viewer" => o%{"todos" => [ + o(%{"totalCount" => 1, "completedCount" => 2}), + o%{"totalCount" => 3, "completedCount" => 4} + ]}} + assert {:ok, %{data: expected}} == Absinthe.run(doc, Schema) + end + + test "it deep merges duplicated fields properly" do + doc = """ + { + viewer { + ...fragmentWithOneField + ...fragmentWithOtherField + } + } + + fragment fragmentWithOneField on User { + todos { + totalCount + completedCount + } + } + + fragment fragmentWithOtherField on User { + todos { + completedCount + } + } + """ + expected = o%{"viewer" => o%{"todos" => [ + o(%{"totalCount" => 1, "completedCount" => 2}), + o%{"totalCount" => 3, "completedCount" => 4} + ]}} + assert {:ok, %{data: expected}} == Absinthe.run(doc, Schema) + end + + test "it deep merges fields properly different levels" do + doc = """ + { + viewer { + ...fragmentWithOneField + } + ...fragmentWithOtherField + } + + fragment fragmentWithOneField on User { + todos { + totalCount, + } + } + + fragment fragmentWithOtherField on RootQueryType { + viewer { + todos { + completedCount + } + } + } + """ + expected = o%{"viewer" => o%{"todos" => [ + o(%{"totalCount" => 1, "completedCount" => 2}), + o%{"totalCount" => 3, "completedCount" => 4} + ]}} + assert {:ok, %{data: expected}} == Absinthe.run(doc, Schema) + end + +end diff --git a/test/lib/absinthe/ordered_introspection_test.exs b/test/lib/absinthe/ordered_introspection_test.exs new file mode 100644 index 0000000000..2787bc4030 --- /dev/null +++ b/test/lib/absinthe/ordered_introspection_test.exs @@ -0,0 +1,484 @@ +defmodule Absinthe.OrderedIntrospectionTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + import AssertResult + + alias Absinthe.Schema + + context "introspection of an object" do + it "returns the name of the object type currently being queried without an alias" do + result = "{ person { __typename name } }" |> run(ContactSchema) + assert_result {:ok, %{data: o%{"person" => o%{"__typename" => "Person", "name" => "Bruce"}}}}, result + end + it "returns the name of the object type currently being queried witho an alias" do + result = "{ person { kind: __typename name } }" |> run(ContactSchema) + assert_result {:ok, %{data: o%{"person" => o%{"kind" => "Person", "name" => "Bruce"}}}}, result + end + end + + context "introspection of an interface" do + it "returns the name of the object type currently being queried" do + # Without an alias + result = "{ contact { entity { __typename name } } }" |> run(ContactSchema) + assert_result {:ok, %{data: o%{"contact" => o%{"entity" => o%{"__typename" => "Person", "name" => "Bruce"}}}}}, result + # With an alias + result = "{ contact { entity { kind: __typename name } } }" |> run(ContactSchema) + assert_result {:ok, %{data: o%{"contact" => o%{"entity" => o%{"kind" => "Person", "name" => "Bruce"}}}}}, result + end + end + + context "when querying against a union" do + it "returns the name of the object type currently being queried" do + # Simple type + result = "{ firstSearchResult { __typename } }" |> run(ContactSchema) + assert_result {:ok, %{data: o(%{"firstSearchResult" => o%{"__typename" => "Person"}})}}, result + # Wrapped type + result = "{ searchResults { __typename } }" |> run(ContactSchema) + assert_result {:ok, %{data: o(%{"searchResults" => [o(%{"__typename" => "Person"}), o(%{"__typename" => "Business"})]})}}, result + end + end + + context "introspection of a schema" do + + it "can use __schema to get types" do + result = "{ __schema { types { name } } }" |> run(ContactSchema) + types = result + |> elem(1) + |> Map.get(:data) + |> OrdMap.get("__schema") + |> OrdMap.get("types") + names = types |> Enum.map(&(OrdMap.get(&1, "name"))) |> Enum.sort + expected = ~w(Int ID String Boolean Float Contact Person Business ProfileInput SearchResult Name NamedEntity RootMutationType RootQueryType RootSubscriptionType __Schema __Directive __DirectiveLocation __EnumValue __Field __InputValue __Type) + |> Enum.sort + assert expected == names + end + + it "can use __schema to get the query type" do + result = "{ __schema { queryType { name kind } } }" |> run(ContactSchema) + assert_result {:ok, %{data: o%{"__schema" => o%{"queryType" => o%{"name" => "RootQueryType", "kind" => "OBJECT"}}}}}, result + end + + it "can use __schema to get the mutation type" do + result = "{ __schema { mutationType { name kind } } }" |> run(ContactSchema) + assert_result {:ok, %{data: o%{"__schema" => o%{"mutationType" => o%{"name" => "RootMutationType", "kind" => "OBJECT"}}}}}, result + end + + it "can use __schema to get the subscription type" do + result = "{ __schema { subscriptionType { name kind } } }" |> Absinthe.run(ContactSchema) + assert_result {:ok, %{data: o%{"__schema" => o%{"subscriptionType" => o%{"name" => "RootSubscriptionType", "kind" => "OBJECT"}}}}}, result + end + + it "can use __schema to get the directives" do + result = """ + { + __schema { + directives { + args { name type { kind ofType { name kind } } } + name + locations + onField + onFragment + onOperation + } + } + } + """ |> run(ContactSchema) + assert {:ok, %{data: o%{"__schema" => o%{"directives" => [ + o(%{"args" => [ + o%{"name" => "if", "type" => o%{"kind" => "NON_NULL", "ofType" => o%{"name" => "Boolean", "kind" => "SCALAR"}}} + ], "name" => "include", "locations" => [ + "INLINE_FRAGMENT", "FRAGMENT_SPREAD", "FIELD" + ], "onField" => true, "onFragment" => true, "onOperation" => false}), + o(%{"args" => [ + o%{"name" => "if", "type" => o%{"kind" => "NON_NULL", "ofType" => o%{"name" => "Boolean", "kind" => "SCALAR"}}} + ], "name" => "skip", "locations" => [ + "INLINE_FRAGMENT", "FRAGMENT_SPREAD", "FIELD" + ], "onField" => true, "onFragment" => true, "onOperation" => false}) + ]}}}} == result + end + + end + + context "introspection of an enum type" do + + it "can use __type and value information with deprecations" do + result = """ + { + __type(name: "Channel") { + name + description + kind + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + } + } + """ + |> run(ColorSchema) + expected = {:ok, %{data: o(%{"__type" => o(%{"name" => "Channel", "description" => "A color channel", "kind" => "ENUM", "enumValues" => [ + o(%{"name" => "BLUE", "description" => "The color blue", "isDeprecated" => false, "deprecationReason" => nil}), + o(%{"name" => "GREEN", "description" => "The color green", "isDeprecated" => false, "deprecationReason" => nil}), + o(%{"name" => "PUCE", "description" => "The color puce", "isDeprecated" => true, "deprecationReason" => "it's ugly"}), + o(%{"name" => "RED", "description" => "The color red", "isDeprecated" => false, "deprecationReason" => nil}) + ]})})}} + assert expected == result + end + + it "can use __type and value information without deprecations" do + result = """ + { + __type(name: "Channel") { + name + description + kind + enumValues { + name + description + } + } + } + """ + |> run(ColorSchema) + assert {:ok, %{data: o%{"__type" => o%{"name" => "Channel", "description" => "A color channel", "kind" => "ENUM", "enumValues" => [ + o(%{"name" => "BLUE", "description" => "The color blue"}), + o(%{"name" => "GREEN", "description" => "The color green"}), + o%{"name" => "RED", "description" => "The color red"} + ]}}}} = result + end + + it "when used as the defaultValue of an argument" do + result = """ + { + __schema { + queryType { + fields { + name + type { + name + } + args { + name + defaultValue + } + } + } + } + } + """ + |> run(ColorSchema) + expected = {:ok, %{data: o%{"__schema" => o%{"queryType" => o%{"fields" => [ + o%{"name" => "info", "type" => o(%{"name" => "ChannelInfo"}), "args" => [o%{"name" => "channel", "defaultValue" => "RED"}]} + ]}}}}} + assert expected == result + end + + it "when used as the default value of an input object" do + result = """ + { + __type(name: "ChannelInput") { + name + inputFields { + name + defaultValue + } + } + } + """ + |> run(ColorSchema) + assert {:ok, %{data: o%{"__type" => o%{"name" => "ChannelInput", "inputFields" => input_fields}}}} = result + assert [ + o%{"name" => "channel", "defaultValue" => "RED"} + ] = input_fields + end + end + + context "introspection of an input object type" do + + it "can use __type and ignore deprecated fields" do + result = """ + { + __type(name: "ProfileInput") { + description + inputFields { + defaultValue + description + name + type { + kind + name + ofType { + kind + name + } + } + } + kind + name + } + } + """ + |> run(ContactSchema) + expected = {:ok, %{data: o%{"__type" => o%{"description" => "The basic details for a person", "inputFields" => [ + o(%{"defaultValue" => "43", "description" => "The person's age", "name" => "age", "type" => o%{"kind" => "SCALAR", "name" => "Int", "ofType" => nil}}), + o(%{"defaultValue" => nil, "description" => nil, "name" => "code", "type" => o%{"kind" => "NON_NULL", "name" => nil, "ofType" => o%{"kind" => "SCALAR", "name" => "String"}}}), + o%{"defaultValue" => "\"Janet\"", "description" => "The person's name", "name" => "name", "type" => o%{"kind" => "SCALAR", "name" => "String", "ofType" => nil}} + ], "kind" => "INPUT_OBJECT", "name" => "ProfileInput"}}}} + assert expected == result + end + + end + + context "introspection of an interface type" do + + it "can use __type and get possible types" do + result = """ + { + __type(name: "NamedEntity") { + description + kind + name + possibleTypes { + name + } + } + } + """ + |> run(ContactSchema) + assert_result {:ok, %{data: o%{"__type" => o%{"description" => "A named entity", "kind" => "INTERFACE", "name" => "NamedEntity", "possibleTypes" => [ + o(%{"name" => "Person"}), o%{"name" => "Business"} + ]}}}}, result + end + + end + + context "introspection of an object type that includes a list" do + + it "can use __type and see fields with the wrapping list types" do + result = """ + { + __type(name: "Person") { + fields(include_deprecated: true) { + name + type { + kind + name + ofType { + kind + name + } + } + } + } + } + """ + |> run(ContactSchema) + assert_result {:ok, + %{data: + o%{"__type" => o%{ + "fields" => [ + o(%{"name" => "address", "type" => o%{"kind" => "SCALAR", "name" => "String", "ofType" => nil}}), + o(%{"name" => "age", "type" => o%{"kind" => "SCALAR", "name" => "Int", "ofType" => nil}}), + o(%{"name" => "name", "type" => o%{"kind" => "SCALAR", "name" => "String", "ofType" => nil}}), + o%{"name" => "others", "type" => o%{"kind" => "LIST", "name" => nil, "ofType" => o%{"kind" => "OBJECT", "name" => "Person"}}} + ]}}}}, result + end + + end + + + context "introspection of an object type" do + + it "can use __type and ignore deprecated fields" do + result = """ + { + __type(name: "Person") { + name + description + kind + fields { + name + } + } + } + """ + |> run(ContactSchema) + assert_result {:ok, %{data: o%{"__type" => o%{"name" => "Person", "description" => "A person", "kind" => "OBJECT", "fields" => [ + o(%{"name" => "age"}), o(%{"name" => "name"}), o(%{"name" => "others"}) + ]}}}}, result + end + + it "can use __type and include deprecated fields" do + result = """ + { + __type(name: "Person") { + description + fields(includeDeprecated: true) { + deprecationReason + isDeprecated + name + } + kind + name + } + } + """ + |> run(ContactSchema) + assert_result {:ok, %{data: o%{"__type" => o%{"description" => "A person", "fields" => [ + o(%{"deprecationReason" => "change of privacy policy", "isDeprecated" => true, "name" => "address"}), + o(%{"deprecationReason" => nil, "isDeprecated" => false, "name" => "age"}), + o(%{"deprecationReason" => nil, "isDeprecated" => false, "name" => "name"}), + o%{"deprecationReason" => nil, "isDeprecated" => false, "name" => "others"} + ], "kind" => "OBJECT", "name" => "Person"}}}}, result + end + + it "can use __type to view interfaces" do + result = """ + { + __type(name: "Person") { + interfaces { + name + } + } + } + """ + |> run(ContactSchema) + assert_result {:ok, %{data: o%{"__type" => o%{"interfaces" => [o%{"name" => "NamedEntity"}]}}}}, result + end + + defmodule KindSchema do + use Absinthe.Schema + + query do + field :foo, :foo + end + + object :foo do + field :name, :string + field :kind, :string + end + + end + + it "can use __type with a field named 'kind'" do + result = """ + { + __type(name: "Foo") { + fields { + name + type { + kind + name + } + } + name + } + } + """ + |> run(KindSchema) + assert {:ok, %{data: o%{ + "__type" => o%{"fields" => [ + o(%{"name" => "kind", "type" => o%{"kind" => "SCALAR", "name" => "String"}}), + o(%{"name" => "name", "type" => o%{"kind" => "SCALAR", "name" => "String"}}) + ], + "name" => "Foo"}} + }} = result + end + + it "can use __schema with a field named 'kind'" do + result = """ + { + __schema { + queryType { + fields { + name + type { + name + kind + } + } + } + } + } + """ + |> run(KindSchema) + assert {:ok, %{data: o%{"__schema" => o%{"queryType" => o%{"fields" => [o%{"name" => "foo", "type" => o%{"name" => "Foo", "kind" => "OBJECT"}}]}}}}} = result + end + + + end + + + defmodule MySchema do + use Absinthe.Schema + + query do + field :greeting, + type: :string, + description: "A traditional greeting", + resolve: fn + _, _ -> {:ok, "Hah!"} + end + end + + end + + context "introspection of a scalar type" do + it "can use __type" do + result = """ + { + __type(name: "String") { + name + description, + kind + fields { + name + } + } + } + """ + |> run(MySchema) + string = Schema.lookup_type(MySchema, :string) + assert_result {:ok, %{data: o%{"__type" => o%{"name" => string.name, "description" => string.description, "kind" => "SCALAR", "fields" => nil}}}}, result + end + end + + + context "introspection of a union type" do + + it "can use __type and get possible types" do + result = """ + { + __type(name: "SearchResult") { + description + kind + name + possibleTypes { + name + } + } + } + """ + |> run(ContactSchema) + assert_result {:ok, %{data: o%{"__type" => o%{"description" => "A search result", "kind" => "UNION", "name" => "SearchResult", "possibleTypes" => [ + o(%{"name" => "Business"}), o%{"name" => "Person"} + ]}}}}, result + end + + end + + context "full introspection" do + + @filename "graphql/introspection.graphql" + @query File.read!(Path.join([:code.priv_dir(:absinthe), @filename])) + + it "runs" do + result = @query |> run(ContactSchema) + assert {:ok, %{data: o%{"__schema" => _}}} = result + end + + end + +end diff --git a/test/lib/absinthe/ordered_schema_test.exs b/test/lib/absinthe/ordered_schema_test.exs new file mode 100644 index 0000000000..4685586790 --- /dev/null +++ b/test/lib/absinthe/ordered_schema_test.exs @@ -0,0 +1,339 @@ +defmodule Absinthe.OrderedSchemaTest do + use Absinthe.Case, async: false, ordered: true + use SupportSchemas + use OrdMap + import AssertResult + + alias Absinthe.Schema + alias Absinthe.Type + + context "built-in types" do + + def load_valid_schema do + load_schema("valid_schema") + end + + it "are loaded" do + load_valid_schema() + assert map_size(Absinthe.Type.BuiltIns.__absinthe_types__) > 0 + Absinthe.Type.BuiltIns.__absinthe_types__ + |> Enum.each(fn + {ident, name} -> + assert ValidSchema.__absinthe_type__(ident) == ValidSchema.__absinthe_type__(name) + end) + int = ValidSchema.__absinthe_type__(:integer) + assert 1 == Type.Scalar.serialize(int, 1) + assert {:ok, 1} == Type.Scalar.parse(int, 1, %{}) + end + + end + + context "using the same identifier" do + + it "raises an exception" do + assert_schema_error("schema_with_duplicate_identifiers", + [%{rule: Absinthe.Schema.Rule.TypeNamesAreUnique, data: %{artifact: "Absinthe type identifier", value: :person}}]) + end + + end + + context "using the same name" do + + def load_duplicate_name_schema do + load_schema("schema_with_duplicate_names") + end + + it "raises an exception" do + assert_schema_error("schema_with_duplicate_names", + [%{rule: Absinthe.Schema.Rule.TypeNamesAreUnique, data: %{artifact: "Type name", value: "Person"}}]) + end + + end + + defmodule SourceSchema do + use Absinthe.Schema + + @desc "can describe query" + query do + field :foo, + type: :foo, + resolve: fn + _, _ -> {:ok, %{name: "Fancy Foo!"}} + end + end + + object :foo do + field :name, :string + end + + end + + defmodule UserSchema do + use Absinthe.Schema + + import_types SourceSchema + + query do + field :foo, + type: :foo, + resolve: fn + _, _ -> {:ok, %{name: "A different fancy Foo!"}} + end + + field :bar, + type: :bar, + resolve: fn + _, _ -> {:ok, %{name: "A plain old bar"}} + end + + end + + object :bar do + field :name, :string + end + + end + + defmodule ThirdSchema do + use Absinthe.Schema + + query do + #Query type must exist + end + + import_types UserSchema + + object :baz do + field :name, :string + end + + end + + it "can have a description on the root query" do + assert "can describe query" == Absinthe.Schema.lookup_type(SourceSchema, :query).description + end + + + context "using import_types" do + + it "adds the types from a parent" do + assert %{foo: "Foo", bar: "Bar"} = UserSchema.__absinthe_types__ + assert "Foo" == UserSchema.__absinthe_type__(:foo).name + end + + it "adds the types from a grandparent" do + assert %{foo: "Foo", bar: "Bar", baz: "Baz"} = ThirdSchema.__absinthe_types__ + assert "Foo" == ThirdSchema.__absinthe_type__(:foo).name + end + + end + + context "lookup_type" do + + it "is supported" do + assert "Foo" == Schema.lookup_type(ThirdSchema, :foo).name + end + + end + + defmodule RootsSchema do + use Absinthe.Schema + + import_types SourceSchema + + query do + + field :name, + type: :string, + args: [ + family_name: [type: :boolean] + ] + + end + + mutation name: "MyRootMutation" do + field :name, :string + end + + subscription name: "RootSubscriptionTypeThing" do + field :name, :string + end + + end + + + context "root fields" do + + it "can have a default name" do + assert "RootQueryType" == Schema.lookup_type(RootsSchema, :query).name + end + + it "can have a custom name" do + assert "MyRootMutation" == Schema.lookup_type(RootsSchema, :mutation).name + end + + it "supports subscriptions" do + assert "RootSubscriptionTypeThing" == Schema.lookup_type(RootsSchema, :subscription).name + end + + + end + + context "fields" do + + it "have the correct structure in query" do + assert %Type.Field{name: "name"} = Schema.lookup_type(RootsSchema, :query).fields.name + end + + it "have the correct structure in subscription" do + assert %Type.Field{name: "name"} = Schema.lookup_type(RootsSchema, :subscription).fields.name + end + + end + + context "arguments" do + + it "have the correct structure" do + assert %Type.Argument{name: "family_name"} = Schema.lookup_type(RootsSchema, :query).fields.name.args.family_name + end + + end + + defmodule FragmentSpreadSchema do + use Absinthe.Schema + + @viewer %{id: "ABCD", name: "Bruce"} + + query do + field :viewer, :viewer do + resolve fn _, _ -> {:ok, @viewer} end + end + end + + object :viewer do + field :id, :id + field :name, :string + end + + end + + context "multiple fragment spreads" do + + @query """ + query Viewer{viewer{id,...F1}} + fragment F0 on Viewer{name,id} + fragment F1 on Viewer{id,...F0} + """ + it "builds the correct result" do + assert_result {:ok, %{data: o%{"viewer" => o%{"id" => "ABCD", "name" => "Bruce"}}}}, run(@query, FragmentSpreadSchema) + end + + end + + + defmodule MetadataSchema do + use Absinthe.Schema + + query do + #Query type must exist + end + + object :foo do + meta :sql_table, "foos" + field :bar, :string do + meta :nice, "yup" + end + end + + input_object :input_foo do + meta :is_input, true + field :bar, :string do + meta :nice, "nope" + end + end + + enum :color do + meta :rgb_only, true + value :red + value :blue + value :green + end + + scalar :my_scalar do + meta :is_scalar, true + # Missing parse and serialize + end + + interface :named do + meta :is_interface, true + field :name, :string do + meta :is_name, true + end + end + + union :result do + types [:foo] + meta :is_union, true + end + + end + + context "can add metadata to an object" do + + it "sets object metadata" do + foo = Schema.lookup_type(MetadataSchema, :foo) + assert %{__private__: [meta: [sql_table: "foos"]]} = foo + assert Type.meta(foo, :sql_table) == "foos" + end + + it "sets field metadata" do + foo = Schema.lookup_type(MetadataSchema, :foo) + assert %{__private__: [meta: [nice: "yup"]]} = foo.fields[:bar] + assert Type.meta(foo.fields[:bar], :nice) == "yup" + end + + it "sets input object metadata" do + input_foo = Schema.lookup_type(MetadataSchema, :input_foo) + assert %{__private__: [meta: [is_input: true]]} = input_foo + assert Type.meta(input_foo, :is_input) == true + end + + it "sets input object field metadata" do + input_foo = Schema.lookup_type(MetadataSchema, :input_foo) + assert %{__private__: [meta: [nice: "nope"]]} = input_foo.fields[:bar] + assert Type.meta(input_foo.fields[:bar], :nice) == "nope" + end + + it "sets enum metadata" do + color = Schema.lookup_type(MetadataSchema, :color) + assert %{__private__: [meta: [rgb_only: true]]} = color + assert Type.meta(color, :rgb_only) == true + end + + it "sets scalar metadata" do + my_scalar = Schema.lookup_type(MetadataSchema, :my_scalar) + assert %{__private__: [meta: [is_scalar: true]]} = my_scalar + assert Type.meta(my_scalar, :is_scalar) == true + end + + it "sets interface metadata" do + named = Schema.lookup_type(MetadataSchema, :named) + assert %{__private__: [meta: [is_interface: true]]} = named + assert Type.meta(named, :is_interface) == true + end + + it "sets interface field metadata" do + named = Schema.lookup_type(MetadataSchema, :named) + assert %{__private__: [meta: [is_name: true]]} = named.fields[:name] + assert Type.meta(named.fields[:name], :is_name) == true + end + + it "sets union metadata" do + result = Schema.lookup_type(MetadataSchema, :result) + assert %{__private__: [meta: [is_union: true]]} = result + assert Type.meta(result, :is_union) == true + end + + end + +end diff --git a/test/lib/absinthe/ordered_union_fragment_test.exs b/test/lib/absinthe/ordered_union_fragment_test.exs new file mode 100644 index 0000000000..370aad0f74 --- /dev/null +++ b/test/lib/absinthe/ordered_union_fragment_test.exs @@ -0,0 +1,150 @@ +defmodule Absinthe.OrderedUnionFragmentTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + defmodule Schema do + use Absinthe.Schema + + object :user do + field :name, :string do + resolve fn user, _, _ -> {:ok, user.username} end + end + field :todos, list_of(:todo) + interface :named + end + + object :todo do + field :name, :string do + resolve fn todo, _, _ -> {:ok, todo.title} end + end + field :completed, :boolean + interface :named + interface :completable + end + + union :object do + types [:user, :todo] + resolve_type fn %{type: type}, _ -> type end + end + + interface :named do + field :name, :string + resolve_type fn %{type: type}, _ -> type end + end + + interface :completable do + field :completed, :boolean + resolve_type fn %{type: type}, _ -> type end + end + + object :viewer do + field :objects, list_of(:object) + field :me, :user + field :named_thing, :named + end + + query do + field :viewer, :viewer do + resolve fn _, _ -> + {:ok, %{ + objects: [ + %{type: :user, username: "foo", completed: true}, + %{type: :todo, title: "do stuff", completed: false}, + %{type: :user, username: "bar"}, + ], + me: %{type: :user, username: "baz", todos: [], name: "should not be exposed"}, + named_thing: %{type: :todo, title: "do stuff", completed: false} + }} + end + end + end + end + + test "it queries a heterogeneous list properly" do + doc = """ + { + viewer { + objects { + ... on User { + __typename + name + } + ... on Todo { + __typename + completed + } + } + } + } + + """ + expected = o%{"viewer" => o%{"objects" => [ + o(%{"__typename" => "User", "name" => "foo"}), + o(%{"__typename" => "Todo", "completed" => false}), + o%{"__typename" => "User", "name" => "bar"} + ]}} + assert {:ok, %{data: expected}} == Absinthe.run(doc, Schema) + end + + test "it queries an interface with the concrete type's field resolvers" do + doc = """ + { + viewer { + me { + ... on Named { + __typename + name + } + } + } + } + + """ + expected = o%{"viewer" => o%{"me" => o%{"__typename" => "User", "name" => "baz"}}} + assert {:ok, %{data: expected}} == Absinthe.run(doc, Schema) + end + + test "it queries an interface implemented by a union type" do + doc = """ + { + viewer { + objects { + ... on Named { + __typename + name + } + } + } + } + + """ + expected = o%{"viewer" => o%{"objects" => [ + o(%{"__typename" => "User", "name" => "foo"}), + o(%{"__typename" => "Todo", "name" => "do stuff"}), + o%{"__typename" => "User", "name" => "bar"} + ]}} + assert {:ok, %{data: expected}} == Absinthe.run(doc, Schema) + end + + test "it queries an interface on an unrelated interface" do + doc = """ + { + viewer { + namedThing { + __typename + name + ... on Completable { + completed + } + } + } + } + + """ + expected = o%{"viewer" => o%{"namedThing" => + o%{"__typename" => "Todo", "name" => "do stuff", "completed" => false} + }} + assert {:ok, %{data: expected}} == Absinthe.run(doc, Schema) + end + +end diff --git a/test/lib/absinthe/resolution/middleware_test.exs b/test/lib/absinthe/resolution/middleware_test.exs index 216827c329..bbe1253a2b 100644 --- a/test/lib/absinthe/resolution/middleware_test.exs +++ b/test/lib/absinthe/resolution/middleware_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.MiddlewareTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false defmodule Auth do def call(res, _) do @@ -90,7 +90,7 @@ defmodule Absinthe.MiddlewareTest do object :user do field :email, :string do middleware MiddlewareTest.Auth - middleware Absinthe.Middleware.MapGet, :email + middleware {Absinthe.Utils.getDefaultMiddleware(), :call}, :email middleware fn res, _ -> # no-op, mostly making sure this form works res diff --git a/test/lib/absinthe/resolution/ordered_middleware_test.exs b/test/lib/absinthe/resolution/ordered_middleware_test.exs new file mode 100644 index 0000000000..cea347b7a8 --- /dev/null +++ b/test/lib/absinthe/resolution/ordered_middleware_test.exs @@ -0,0 +1,159 @@ +defmodule Absinthe.OrderedMiddlewareTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + defmodule Auth do + def call(res, _) do + case res.context do + %{current_user: _} -> + res + _ -> + res + |> Absinthe.Resolution.put_result({:error, "unauthorized"}) + end + end + end + + defmodule Schema do + use Absinthe.Schema + + alias Absinthe.MiddlewareTest + + def middleware(middleware, _field, %Absinthe.Type.Object{identifier: :secret_object}) do + fun = &auth/2 # can't inline due to Elixir bug. + [ fun | middleware] + end + def middleware(middleware, _field, _) do + middleware + end + + def auth(res, _) do + case res.context do + %{current_user: _} -> + res + _ -> + res + |> Absinthe.Resolution.put_result({:error, "unauthorized"}) + end + end + + query do + field :authenticated, :user do + middleware MiddlewareTest.Auth + + resolve fn _, _, _ -> + {:ok, %{name: "bob"}} + end + end + + field :public, :user do + resolve fn _, _, _ -> + {:ok, %{name: "bob", email: "secret"}} + end + end + + field :returns_private_object, :secret_object do + resolve fn _, _, _ -> + {:ok, %{key: "value"}} + end + end + + field :from_context, :string do + middleware fn res, _ -> + %{res | context: %{value: "yooooo"}} + end + + resolve fn _, %{context: context} -> + {:ok, context.value} + end + end + + field :path, :path do + resolve fn _, _ -> {:ok, %{}} end + end + end + + object :path do + field :path, :path, resolve: fn _, _ -> {:ok, %{}} end + field :result, list_of(:string) do + resolve fn _, info -> + {:ok, Absinthe.Resolution.path_string(info)} + end + end + end + + # keys in this object are made secret via the def middleware callback + object :secret_object do + field :key, :string + field :key2, :string + end + + object :user do + field :email, :string do + middleware MiddlewareTest.Auth + middleware {Absinthe.Utils.getDefaultMiddleware(), :call}, :email + middleware fn res, _ -> + # no-op, mostly making sure this form works + res + end + end + field :name, :string + end + end + + test "fails with authorization error when no current user" do + doc = """ + {authenticated { name }} + """ + assert {:ok, %{errors: errors}} = Absinthe.run(doc, __MODULE__.Schema) + assert [%{locations: [%{column: 0, line: 1}], message: "unauthorized", path: ["authenticated"]}] == errors + end + + test "email fails with authorization error when no current user" do + doc = """ + {public { name email }} + """ + assert {:ok, %{errors: errors}} = Absinthe.run(doc, __MODULE__.Schema) + assert [%{locations: [%{column: 0, line: 1}], message: "unauthorized", path: ["public", "email"]}] == errors + end + + test "email works when current user" do + doc = """ + {public { name email }} + """ + assert {:ok, %{data: data}} = Absinthe.run(doc, __MODULE__.Schema, context: %{current_user: %{}}) + assert o(%{"public" => o(%{"name" => "bob", "email" => "secret"})}) == data + end + + test "secret object cant be accessed without a current user" do + doc = """ + {returnsPrivateObject { key }} + """ + assert {:ok, %{errors: errors}} = Absinthe.run(doc, __MODULE__.Schema) + assert [%{locations: [%{column: 0, line: 1}], + message: "unauthorized", path: ["returnsPrivateObject", "key"]}] == errors + end + + test "secret object can be accessed with a current user" do + doc = """ + {returnsPrivateObject { key }} + """ + assert {:ok, %{data: o%{"returnsPrivateObject" => o%{"key" => "value"}}}} == Absinthe.run(doc, __MODULE__.Schema, context: %{current_user: %{}}) + end + + test "it can modify the context" do + doc = """ + {fromContext} + """ + assert {:ok, %{data: data}} = Absinthe.run(doc, __MODULE__.Schema, context: %{current_user: %{}}) + assert o(%{"fromContext" => "yooooo"}) == data + end + + test "it gets the path of the current field" do + doc = """ + {foo: path { bar: path { result }}} + """ + assert {:ok, %{data: data}} = Absinthe.run(doc, __MODULE__.Schema, context: %{current_user: %{}}) + assert o(%{"foo" => o(%{"bar" => o(%{"result" => ["result", "bar", "foo", "RootQueryType"]})})}) == data + end +end diff --git a/test/lib/absinthe/schema_test.exs b/test/lib/absinthe/schema_test.exs index 1bb003d15e..cadf23319c 100644 --- a/test/lib/absinthe/schema_test.exs +++ b/test/lib/absinthe/schema_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.SchemaTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false use SupportSchemas import AssertResult diff --git a/test/lib/absinthe/type/directive_test.exs b/test/lib/absinthe/type/directive_test.exs index a4b7b9959f..0add8400b2 100644 --- a/test/lib/absinthe/type/directive_test.exs +++ b/test/lib/absinthe/type/directive_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.Type.DirectiveTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false alias Absinthe.Schema import AssertResult diff --git a/test/lib/absinthe/type/interface_test.exs b/test/lib/absinthe/type/interface_test.exs index 6ae254391f..9fbf40f2e0 100644 --- a/test/lib/absinthe/type/interface_test.exs +++ b/test/lib/absinthe/type/interface_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.Type.InterfaceTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false import AssertResult use SupportSchemas diff --git a/test/lib/absinthe/type/ordered_directive_test.exs b/test/lib/absinthe/type/ordered_directive_test.exs new file mode 100644 index 0000000000..360c43b0a1 --- /dev/null +++ b/test/lib/absinthe/type/ordered_directive_test.exs @@ -0,0 +1,144 @@ +defmodule Absinthe.Type.OrderedDirectiveTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + + alias Absinthe.Schema + import AssertResult + + defmodule TestSchema do + use Absinthe.Schema + + query do + field :nonce, :string + end + + end + + context "directives" do + it "are loaded as built-ins" do + assert %{skip: "skip", include: "include"} = TestSchema.__absinthe_directives__ + assert TestSchema.__absinthe_directive__(:skip) + assert TestSchema.__absinthe_directive__("skip") == TestSchema.__absinthe_directive__(:skip) + assert Schema.lookup_directive(TestSchema, :skip) == TestSchema.__absinthe_directive__(:skip) + assert Schema.lookup_directive(TestSchema, "skip") == TestSchema.__absinthe_directive__(:skip) + end + + end + + context "the `@skip` directive" do + @query_field """ + query Test($skipPerson: Boolean) { + person @skip(if: $skipPerson) { + name + } + } + """ + it "is defined" do + assert Schema.lookup_directive(ContactSchema, :skip) + end + it "behaves as expected for a field" do + assert {:ok, %{data: o%{"person" => o%{"name" => "Bruce"}}}} == Absinthe.run(@query_field, ContactSchema, variables: %{"skipPerson" => false}) + assert {:ok, %{data: o%{}}} == Absinthe.run(@query_field, ContactSchema, variables: %{"skipPerson" => true}) + assert_result {:ok, %{errors: [%{message: ~s(In argument "if": Expected type "Boolean!", found null.)}]}}, run(@query_field, ContactSchema) + end + + @query_fragment """ + query Test($skipAge: Boolean) { + person { + name + ...Aging @skip(if: $skipAge) + } + } + fragment Aging on Person { + age + } + """ + it "behaves as expected for a fragment" do + assert_result {:ok, %{data: o%{"person" => o%{"name" => "Bruce", "age" => 35}}}}, run(@query_fragment, ContactSchema, variables: %{"skipAge" => false}) + assert_result {:ok, %{data: o%{"person" => o%{"name" => "Bruce"}}}}, run(@query_fragment, ContactSchema, variables: %{"skipAge" => true}) + assert_result {:ok, %{errors: [%{message: ~s(In argument "if": Expected type "Boolean!", found null.)}]}}, run(@query_fragment, ContactSchema) + end + end + + context "the `@include` directive" do + @query_field """ + query Test($includePerson: Boolean) { + person @include(if: $includePerson) { + name + } + } + """ + it "is defined" do + assert Schema.lookup_directive(ContactSchema, :include) + end + it "behaves as expected for a field" do + assert_result {:ok, %{data: o%{"person" => o%{"name" => "Bruce"}}}}, run(@query_field, ContactSchema, variables: %{"includePerson" => true}) + assert_result {:ok, %{data: o%{}}}, run(@query_field, ContactSchema, variables: %{"includePerson" => false}) + assert_result {:ok, %{errors: [%{locations: [%{column: 0, line: 2}], message: ~s(In argument "if": Expected type "Boolean!", found null.)}]}}, run(@query_field, ContactSchema) + end + + @query_fragment """ + query Test($includeAge: Boolean) { + person { + name + ...Aging @include(if: $includeAge) + } + } + fragment Aging on Person { + age + } + """ + it "behaves as expected for a fragment" do + assert {:ok, %{data: o%{"person" => o%{"name" => "Bruce", "age" => 35}}}} == Absinthe.run(@query_fragment, ContactSchema, variables: %{"includeAge" => true}) + assert {:ok, %{data: o%{"person" => o%{"name" => "Bruce"}}}} == Absinthe.run(@query_fragment, ContactSchema, variables: %{"includeAge" => false}) + end + + it "should return an error if the variable is not supplied" do + assert {:ok, %{errors: errors}} = Absinthe.run(@query_fragment, ContactSchema) + assert [] != errors + end + end + + context "for inline fragments without type conditions" do + + @query """ + query Q($skipAge: Boolean = false) { + person { + name + ... @skip(if: $skipAge) { + age + } + } + } + """ + + it "works as expected" do + assert {:ok, %{data: o%{"person" => o%{"name" => "Bruce"}}}} == Absinthe.run(@query, ContactSchema, variables: %{"skipAge" => true}) + assert {:ok, %{data: o%{"person" => o%{"name" => "Bruce", "age" => 35}}}} == Absinthe.run(@query, ContactSchema, variables: %{"skipAge" => false}) + assert {:ok, %{data: o%{"person" => o%{"name" => "Bruce", "age" => 35}}}} == Absinthe.run(@query, ContactSchema) + end + + end + + context "for inline fragments with type conditions" do + + @query """ + query Q($skipAge: Boolean = false) { + person { + name + ... on Person @skip(if: $skipAge) { + age + } + } + } + """ + + it "works as expected" do + assert {:ok, %{data: o%{"person" => o%{"name" => "Bruce"}}}} == Absinthe.run(@query, ContactSchema, variables: %{"skipAge" => true}) + assert {:ok, %{data: o%{"person" => o%{"name" => "Bruce", "age" => 35}}}} == Absinthe.run(@query, ContactSchema, variables: %{"skipAge" => false}) + assert {:ok, %{data: o%{"person" => o%{"name" => "Bruce", "age" => 35}}}} == Absinthe.run(@query, ContactSchema) + end + + end + +end diff --git a/test/lib/absinthe/type/ordered_interface_test.exs b/test/lib/absinthe/type/ordered_interface_test.exs new file mode 100644 index 0000000000..483ca50fee --- /dev/null +++ b/test/lib/absinthe/type/ordered_interface_test.exs @@ -0,0 +1,209 @@ +defmodule Absinthe.Type.OrderedInterfaceTest do + use Absinthe.Case, async: false, ordered: true + import AssertResult + use SupportSchemas + use OrdMap + + alias Absinthe.Schema.Rule + alias Absinthe.Schema + + defmodule OrderedTestSchema do + use Absinthe.Schema + + query do + field :foo, type: :foo + field :bar, type: :bar + field :named_thing, :named do + resolve fn _, _ -> + {:ok, %{}} + end + end + end + + object :foo do + field :name, :string + is_type_of fn + _ -> + true + end + interface :named + end + + object :bar do + field :name, :string + is_type_of fn + _ -> + true + end + interface :named + end + + # NOT USED IN THE QUERY + object :baz do + field :name, :string + is_type_of fn + _ -> + true + end + interfaces [:named] + end + + interface :named do + description "An interface" + field :name, :string + resolve_type fn + _, _ -> + nil # just a value + end + end + + end + + context "interface" do + + it "can be defined" do + obj = OrderedTestSchema.__absinthe_type__(:named) + assert %Absinthe.Type.Interface{name: "Named", description: "An interface"} = obj + assert obj.resolve_type + end + + it "captures the relationships in the schema" do + implementors = Map.get(OrderedTestSchema.__absinthe_interface_implementors__, :named, []) + assert :foo in implementors + assert :bar in implementors + # Not directly in squery, but because it's + # an available type and there's a field that + # defines the interface as a type + assert :baz in implementors + end + + it "can find implementors" do + obj = OrderedTestSchema.__absinthe_type__(:named) + assert length(Schema.implementors(OrderedTestSchema, obj)) == 3 + end + + end + + context "an object that implements an interface" do + + context "with the interface as a field type" do + + it "can select fields that are declared by the interface" do + result = """ + { contact { entity { name } } } + """ |> Absinthe.run(ContactSchema) + assert_result {:ok, %{data: o%{"contact" => o%{"entity" => o%{"name" => "Bruce"}}}}}, result + end + + it "can't select fields from an implementing type without 'on'" do + result = """ + { contact { entity { name age } } } + """ |> Absinthe.run(ContactSchema) + assert_result {:ok, %{errors: [%{message: ~s(Cannot query field "age" on type "NamedEntity". Did you mean to use an inline fragment on "Person"?)}]}}, result + end + + it "can select fields from an implementing type with 'on'" do + result = """ + { contact { entity { name ... on Person { age } } } } + """ |> Absinthe.run(ContactSchema) + assert_result {:ok, %{data: o%{"contact" => o%{"entity" => o%{"name" => "Bruce", "age" => 35}}}}}, result + end + + end + + end + + context "when it doesn't define those fields" do + + it "reports schema errors" do + assert_schema_error( + "bad_interface_schema", + [ + %{rule: Rule.ObjectMustImplementInterfaces, data: %{object: "Foo", interface: "Aged"}}, + %{rule: Rule.ObjectMustImplementInterfaces, data: %{object: "Foo", interface: "Named"}}, + %{rule: Rule.ObjectInterfacesMustBeValid, data: %{object: "Quux", interface: "Foo"}}, + %{rule: Rule.InterfacesMustResolveTypes, data: "Named"}, + ] + ) + end + end + + it "can query simple InterfaceSubtypeSchema" do + result = """ + { + box { + item { + name + cost + } + } + } + """ + |> run(Absinthe.InterfaceSubtypeSchema) + assert_result {:ok, %{data: o%{"box" => o%{"item" => o%{"name" => "Computer", "cost" => 1000}}}}}, result + end + + it "can query InterfaceSubtypeSchema treating box as HasItem" do + result = """ + { + box { + ... on HasItem { + item { + name + } + } + } + } + """ + |> run(Absinthe.InterfaceSubtypeSchema) + assert_result {:ok, %{data: o%{"box" => o%{"item" => o%{"name" => "Computer"}}}}}, result + end + + it "can query InterfaceSubtypeSchema treating box as HasItem and item as ValuedItem" do + result = """ + { + box { + ... on HasItem { + item { + name + ... on ValuedItem { + cost + } + } + } + } + } + """ + |> run(Absinthe.InterfaceSubtypeSchema) + assert_result {:ok, %{data: o%{"box" => o%{"item" => o%{"name" => "Computer", "cost" => 1000}}}}}, result + end + + it "rejects querying InterfaceSubtypeSchema treating box as HasItem asking for cost" do + result = """ + { + box { + ... on HasItem { + item { + name + cost + } + } + } + } + """ + |> run(Absinthe.InterfaceSubtypeSchema) + assert_result {:ok, %{errors: [%{message: "Cannot query field \"cost\" on type \"Item\". Did you mean to use an inline fragment on \"ValuedItem\"?"}]}}, result + end + + it "works even when resolve_type returns nil" do + result = """ + { + namedThing { + name + } + } + """ + |> run(OrderedTestSchema) + assert_result {:ok, %{data: o%{"namedThing" => o%{}}}}, result + end +end diff --git a/test/lib/absinthe/union_fragment_test.exs b/test/lib/absinthe/union_fragment_test.exs index 0f572e8492..23a6b3e4f3 100644 --- a/test/lib/absinthe/union_fragment_test.exs +++ b/test/lib/absinthe/union_fragment_test.exs @@ -1,5 +1,5 @@ defmodule Absinthe.UnionFragmentTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false defmodule Schema do use Absinthe.Schema diff --git a/test/lib/absinthe_test.exs b/test/lib/absinthe_test.exs index 3ba2227c94..02c820e677 100644 --- a/test/lib/absinthe_test.exs +++ b/test/lib/absinthe_test.exs @@ -1,5 +1,5 @@ defmodule AbsintheTest do - use Absinthe.Case, async: true + use Absinthe.Case, async: false, ordered: false import AssertResult it "can return multiple errors" do @@ -520,4 +520,4 @@ defmodule AbsintheTest do assert_result {:ok, %{errors: [%{message: "Argument \"thing\" has invalid value $input.\nIn field \"__typename\": Unknown field."}]}}, result end -end +end \ No newline at end of file diff --git a/test/lib/ordered_absinthe_test.exs b/test/lib/ordered_absinthe_test.exs new file mode 100644 index 0000000000..c1f8ca81cf --- /dev/null +++ b/test/lib/ordered_absinthe_test.exs @@ -0,0 +1,523 @@ +defmodule OrderedAbsintheTest do + use Absinthe.Case, async: false, ordered: true + use OrdMap + import AssertResult + + it "can return multiple errors" do + query = "mutation { failingThing(type: MULTIPLE) { name } }" + assert_result {:ok, %{data: o(%{"failingThing" => nil}), errors: [%{message: "one", path: ["failingThing"]}, %{message: "two", path: ["failingThing"]}]}}, run(query, OrderedThings) + end + + it "can return extra error fields" do + query = "mutation { failingThing(type: WITH_CODE) { name } }" + assert_result {:ok, %{data: o(%{"failingThing" => nil}), errors: [%{code: 42, message: "Custom Error", path: ["failingThing"]}]}}, run(query, OrderedThings) + end + + it "requires message in extended errors" do + query = "mutation { FailingThing(type: WITHOUT_MESSAGE) { name } }" + assert_raise Absinthe.ExecutionError, fn -> run(query, OrderedThings) end + end + + it "can return multiple errors, with extra error fields" do + query = "mutation { failingThing(type: MULTIPLE_WITH_CODE) { name } }" + assert_result {:ok, %{data: o(%{"failingThing" => nil}), errors: [%{code: 1, message: "Custom Error 1", path: ["failingThing"]}, %{code: 2, message: "Custom Error 2", path: ["failingThing"]}]}}, run(query, OrderedThings) + end + + it "requires message in extended errors, when multiple errors are given" do + query = "mutation { failingThing(type: MULTIPLE_WITHOUT_MESSAGE) { name } }" + assert_raise Absinthe.ExecutionError, fn -> run(query, OrderedThings) end + end + + it "can do a simple query" do + query = """ + query GimmeFoo { + thing(id: "foo") { + name + } + } + """ + assert_result {:ok, %{data: o%{"thing" => o%{"name" => "Foo"}}}}, run(query, OrderedThings) + end + + it "can do a simple query with fragments" do + query = """ + { + ... Fields + } + + fragment Fields on RootQueryType { + thing(id: "foo") { + name + } + } + """ + assert_result {:ok, %{data: o%{"thing" => o%{"name" => "Foo"}}}}, run(query, OrderedThings) + end + + it "can do a simple query with a weird alias" do + query = """ + query GimmeFoo { + thing(id: "foo") { + fOO_Bar_baz: name + } + } + """ + assert_result {:ok, %{data: o%{"thing" => o%{"fOO_Bar_baz" => "Foo"}}}}, run(query, OrderedThings) + end + + it "can do a simple query returning a list" do + query = """ + query AllTheThings { + things { + name + id + } + } + """ + assert_result {:ok, %{data: o%{"things" => [o(%{"name" => "Bar", "id" => "bar"}), o%{"name" => "Foo", "id" => "foo"}]}}}, run(query, OrderedThings) + end + + it "returns an error message when a list is given where it doesn't belong" do + query = """ + query GimmeFoo { + thing(id: ["foo"]) { + name + } + } + """ + assert_result {:ok, %{errors: [%{locations: [%{column: 0, line: 2}], + message: "Argument \"id\" has invalid value [\"foo\"]."}]}}, run(query, OrderedThings) + end + + it "Invalid arguments on children of a list field are correctly handled" do + query = """ + query AllTheThings { + things { + id(x: 1) + name + } + } + """ + assert_result {:ok, %{errors: [%{message: "Unknown argument \"x\" on field \"id\" of type \"Thing\"."}]}}, run(query, OrderedThings) + end + + it "can do a simple query with an all caps alias" do + query = """ + query GimmeFoo { + thing(id: "foo") { + FOO: name + } + } + """ + assert_result {:ok, %{data: o%{"thing" => o%{"FOO" => "Foo"}}}}, run(query, OrderedThings) + end + + it "can identify a bad field" do + query = """ + { + thing(id: "foo") { + name + bad + } + } + """ + assert_result {:ok, %{errors: [%{message: ~s(Cannot query field "bad" on type "Thing".)}]}}, run(query, OrderedThings) + end + + it "blows up on bad resolutions" do + query = """ + { + badResolution { + name + } + } + """ + assert_raise Absinthe.ExecutionError, fn -> run(query, OrderedThings) end + end + + it "returns the correct results for an alias" do + query = """ + query GimmeFooByAlias { + widget: thing(id: "foo") { + name + } + } + """ + assert_result {:ok, %{data: o%{"widget" => o%{"name" => "Foo"}}}}, run(query, OrderedThings) + end + + it "checks for required arguments" do + query = "{ thing { name } }" + assert_result {:ok, %{errors: [%{message: ~s(In argument "id": Expected type "String!", found null.)}]}}, run(query, OrderedThings) + end + + it "checks for extra arguments" do + query = """ + { + thing(id: "foo", extra: "dunno") { + name + } + } + """ + assert_result {:ok, %{errors: [%{message: ~s(Unknown argument "extra" on field "thing" of type "RootQueryType".)}]}}, run(query, OrderedThings) + end + + it "checks for badly formed arguments" do + query = """ + { + number(val: "AAA") + } + """ + assert_result {:ok, %{errors: [%{message: ~s(Argument "val" has invalid value "AAA".)}]}}, run(query, OrderedThings) + end + + it "returns nested objects" do + query = """ + query GimmeFooWithOtherThing { + thing(id: "foo") { + name + otherThing { + name + } + } + } + """ + assert_result {:ok, %{data: o%{"thing" => o%{"name" => "Foo", "otherThing" => o%{"name" => "Bar"}}}}}, run(query, OrderedThings) + end + + it "can provide context" do + query = """ + query GimmeThingByContext { + thingByContext { + name + } + } + """ + assert_result {:ok, %{data: o%{"thingByContext" => o%{"name" => "Bar"}}}}, run(query, Things, context: %{thing: "bar"}) + assert_result {:ok, %{data: o(%{"thingByContext" => nil}), + errors: [%{message: ~s(No :id context provided), path: ["thingByContext"]}]}}, run(query, OrderedThings) + end + + it "can use variables" do + query = """ + query GimmeThingByVariable($thingId: String!) { + thing(id: $thingId) { + name + } + } + """ + result = run(query, Things, variables: %{"thingId" => "bar"}) + assert_result {:ok, %{data: o%{"thing" => o%{"name" => "Bar"}}}}, result + end + + it "can handle variable errors without an operation name" do + query = """ + query($userId: String, $test: String) { + user(id: $userId) { + id + } + } + """ + assert_result {:ok, + %{errors: [ + %{message: "Cannot query field \"user\" on type \"RootQueryType\". Did you mean \"number\"?"}, + %{message: "Unknown argument \"id\" on field \"user\" of type \"RootQueryType\"."}, + %{message: "Variable \"test\" is never used."}]} + }, run(query, Things, variables: %{"id" => "foo"}) + end + + it "can use input objects" do + query = """ + mutation UpdateThingValue { + thing: update_thing(id: "foo", thing: {value: 100}) { + name + value + } + } + """ + result = run(query, OrderedThings) + assert_result {:ok, %{data: o%{"thing" => o%{"name" => "Foo", "value" => 100}}}}, result + end + + it "checks for badly formed nested arguments" do + query = """ + mutation UpdateThingValueBadly { + thing: updateThing(id: "foo", thing: {value: "BAD"}) { + name + value + } + } + """ + assert_result {:ok, %{errors: [%{message: ~s(Argument "thing" has invalid value {value: "BAD"}.\nIn field "value": Expected type "Int", found "BAD".)}]}}, run(query, OrderedThings) + end + + it "reports variables that are never used" do + query = """ + query GimmeThingByVariable($thingId: String, $other: String!) { + thing(id: $thingId) { + name + } + } + """ + result = run(query, Things, variables: %{"thingId" => "bar"}) + assert_result { + :ok, + %{ + errors: [ + %{message: "Variable \"other\": Expected non-null, found null."}, + %{message: ~s(Variable "other" is never used in operation "GimmeThingByVariable".)} + ] + } + }, result + end + + it "reports parser errors from parse" do + query = """ + { + thing(id: "foo") {}{ name } + } + """ + assert_result {:ok, %{errors: [%{message: "syntax error before: '}'"}]}}, run(query, OrderedThings) + end + + it "reports parser errors from run" do + query = """ + { + thing(id: "foo") {}{ name } + } + """ + result = run(query, OrderedThings) + assert_result {:ok, %{errors: [%{message: "syntax error before: '}'"}]}}, result + end + + it "Should be retrievable using the ID type as a string" do + result = """ + { + item(id: "foo") { + id + name + } + } + """ + |> run(Absinthe.IdTestSchema) + assert_result {:ok, %{data: o%{"item" => o%{"id" => "foo", "name" => "Foo"}}}}, result + end + + it "should wrap all lexer errors and return if not aborting to a phase" do + query = """ + { + item(this-won't-parse) + } + """ + + assert {:error, bp} = Absinthe.Phase.Parse.run(query, jump_phases: false) + assert [%Absinthe.Phase.Error{extra: %{}, + locations: [%{column: 0, line: 2}], message: "illegal: -w", + phase: Absinthe.Phase.Parse}] == bp.execution.validation_errors + end + + it "should resolve using enums" do + result = """ + { + red: info(channel: RED) { + name + value + } + green: info(channel: GREEN) { + name + value + } + blue: info(channel: BLUE) { + name + value + } + puce: info(channel: PUCE) { + name + value + } + } + """ + |> run(ColorSchema) + assert_result {:ok, %{data: o%{"red" => o(%{"name" => "RED", "value" => 100}), "green" => o(%{"name" => "GREEN", "value" => 200}), "blue" => o(%{"name" => "BLUE", "value" => 300}), "puce" => o%{"name" => "PUCE", "value" => -100}}}}, result + end + + it "should return an error when not specifying subfields" do + query = """ + { + things + } + """ + result = run(query, OrderedThings) + assert_result {:ok, %{errors: [%{message: "Field \"things\" of type \"[Thing]\" must have a selection of subfields. Did you mean \"things { ... }\"?"}]}}, result + end + + context "fragments" do + + @simple_fragment """ + query Q { + person { + ...NamedPerson + } + } + fragment NamedPerson on Person { + name + } + """ + + @unapplied_fragment """ + query Q { + person { + name + ...NamedBusiness + } + } + fragment NamedBusiness on Business { + employee_count + } + """ + + @introspection_fragment """ + query Q { + __type(name: "ProfileInput") { + name + kind + fields { + name + } + ...Inputs + } + } + + fragment Inputs on __Type { + inputFields { name } + } + + """ + + it "can be parsed" do + {:ok, %{input: doc}, _} = Absinthe.Pipeline.run(@simple_fragment, [Absinthe.Phase.Parse]) + assert %{definitions: [%Absinthe.Language.OperationDefinition{}, + %Absinthe.Language.Fragment{name: "NamedPerson"}]} = doc + end + + it "returns the correct result" do + assert_result {:ok, %{data: o%{"person" => o%{"name" => "Bruce"}}}}, run(@simple_fragment, ContactSchema) + end + + it "returns the correct result using fragments for introspection" do + assert {:ok, %{data: o%{"__type" => o%{"name" => "ProfileInput", "kind" => "INPUT_OBJECT", "fields" => nil, "inputFields" => input_fields}}}} = run(@introspection_fragment, ContactSchema) + correct = [o(%{"name" => "code"}), o(%{"name" => "name"}), o(%{"name" => "age"})] + sort = &(OrdMap.get(&1, "name")) + assert Enum.sort_by(input_fields, sort) == Enum.sort_by(correct, sort) + end + + it "Object spreads in object scope should return an error" do + # https://facebook.github.io/graphql/#sec-Object-Spreads-In-Object-Scope + assert {:ok, %{errors: [%{locations: [%{column: 0, line: 4}], message: "Fragment spread has no type overlap with parent.\nParent possible types: [\"Person\"]\nSpread possible types: [\"Business\"]\n"}]}} == run(@unapplied_fragment, ContactSchema) + end + + end + + context "a root_value" do + + @version "1.4.5" + @query "{ version }" + it "is used to resolve toplevel fields" do + assert {:ok, %{data: o%{"version" => @version}}} == run(@query, Things, root_value: %{version: @version}) + end + + end + + context "an alias with an underscore" do + + @query """ + { _thing123:thing(id: "foo") { name } } + """ + it "is returned intact" do + assert {:ok, %{data: o%{"_thing123" => o%{"name" => "Foo"}}}} == run(@query, Things) + end + + end + + context "multiple operation documents" do + @multiple_ops_query """ + query ThingFoo { + thing(id: "foo") { + name + } + } + query ThingBar { + thing(id: "bar") { + name + } + } + """ + + it "can select an operation by name" do + assert {:ok, %{data: o%{"thing" => o%{"name" => "Foo"}}}} == run(@multiple_ops_query, Things, operation_name: "ThingFoo") + end + + it "should error when no operation name is supplied" do + assert {:ok, %{errors: [%{message: "Must provide a valid operation name if query contains multiple operations."}]}} == run(@multiple_ops_query, Things) + end + it "should error when an invalid operation name is supplied" do + op_name = "invalid" + assert_result {:ok, %{errors: [%{message: "Must provide a valid operation name if query contains multiple operations."}]}}, run(@multiple_ops_query, Things, operation_name: op_name) + end + end + + it "handles cycles" do + cycler = """ + query Foo { + name + } + fragment Foo on Blag { + name + ...Bar + } + fragment Bar on Blah { + age + ...Foo + } + """ + assert_result {:ok, %{errors: [%{message: "Cannot spread fragment \"Foo\" within itself via \"Bar\", \"Foo\"."}, %{message: "Cannot spread fragment \"Bar\" within itself via \"Foo\", \"Bar\"."}]}}, run(cycler, Things) + end + + it "can return errors with aliases" do + query = "mutation { foo: failingThing(type: WITH_CODE) { name } }" + assert_result {:ok, %{data: o(%{"foo" => nil}), errors: [%{code: 42, message: "Custom Error", path: ["foo"]}]}}, run(query, OrderedThings) + end + + it "can return errors with indices" do + query = """ + query AllTheThings { + things { + id + fail(id: "foo") + } + } + """ + assert_result {:ok, %{data: o(%{"things" => [o(%{"id" => "bar", "fail" => "bar"}), o(%{"id" => "foo", "fail" => nil})]}), errors: [%{message: "fail", path: ["things", 1, "fail"]}]}}, run(query, OrderedThings) + end + + it "errors when mutations are run without any mutation object" do + query = """ + mutation { foo } + """ + assert_result {:ok, %{errors: [%{message: "Operation \"mutation\" not supported"}]}}, run(query, Absinthe.Test.OnlyQuerySchema) + end + + it "provides proper error when __typename is included in variables" do + query = """ + mutation UpdateThingValue($input: InputThing) { + thing: update_thing(id: "foo", thing: $input) { + name + value + } + } + """ + result = run(query, Things, variables: %{"input" => %{"value" => 100, "__typename" => "foo"}}) + assert_result {:ok, %{errors: [%{message: "Argument \"thing\" has invalid value $input.\nIn field \"__typename\": Unknown field."}]}}, result + end + +end diff --git a/test/support/case.ex b/test/support/case.ex index 3eba2dd817..387c9c5675 100644 --- a/test/support/case.ex +++ b/test/support/case.ex @@ -1,10 +1,39 @@ defmodule Absinthe.Case do defmacro __using__(opts) do + {ordered, opts} = Keyword.pop(opts, :ordered) + {importRun, opts} = Keyword.pop(opts, :import_run, true) + async = Keyword.get(opts, :async) + + if async and is_boolean(ordered) do + IO.puts "\nWARNING: Module #{__CALLER__.module} has set option :ordered => it shouldn't be run with async:true" + end + quote do use ExUnit.Case, unquote(opts) import ExUnit.Case, except: [describe: 2] import ExSpec - import Absinthe.Case.Run + unquote do + if importRun do + quote do + import Absinthe.Case.Run + end + end + end + + unquote do + unless is_nil(ordered) do + quote do + setup_all do + ordered = Application.get_env(:absinthe, :ordered) + Application.put_env(:absinthe, :ordered, unquote(ordered)) + on_exit(nil, fn -> + Application.put_env(:absinthe, :ordered, ordered) + end) + :ok + end + end + end + end Module.put_attribute(__MODULE__, :ex_spec_contexts, []) end diff --git a/test/support/ordered_things.ex b/test/support/ordered_things.ex new file mode 100644 index 0000000000..6855ca93e6 --- /dev/null +++ b/test/support/ordered_things.ex @@ -0,0 +1,200 @@ +defmodule OrderedThings do + use OrdMap + use Absinthe.Schema + + @db o%{ + "foo" => o(%{id: "foo", name: "Foo", value: 4}), + "bar" => o%{id: "bar", name: "Bar", value: 5} + } + + enum :sigils_work, values: ~w(foo bar)a + + enum :sigils_work_inside do + values ~w(foo bar)a + end + + enum :failure_type do + value :multiple + value :with_code + value :without_message + value :multiple_with_code + value :multiple_without_message + end + + mutation do + + field :update_thing, + type: :thing, + args: [ + id: [type: non_null(:string)], + thing: [type: non_null(:input_thing)] + ], + resolve: fn + %{id: id, thing: %{value: val}}, _ -> + found = @db |> OrdMap.get(id) + {:ok, OrdMap.replace(found, :value, val)} + %{id: id, thing: fields}, _ -> + found = @db |> OrdMap.get(id) + {:ok, found |> OrdMap.merge(fields)} + end + + field :failing_thing, type: :thing do + arg :type, type: :failure_type + resolve fn + %{type: :multiple}, _ -> + {:error, ["one", "two"]} + %{type: :with_code}, _ -> + {:error, message: "Custom Error", code: 42} + %{type: :without_message}, _ -> + {:error, code: 42} + %{type: :multiple_with_code}, _ -> + {:error, [%{message: "Custom Error 1", code: 1}, %{message: "Custom Error 2", code: 2}]} + %{type: :multiple_without_message}, _ -> + {:error, [%{message: "Custom Error 1", code: 1}, %{code: 2}]} + end + end + + end + + query do + + field :version, :string + + field :bad_resolution, + type: :thing, + resolve: fn(_, _) -> + :not_expected + end + + field :number, + type: :string, + args: [ + val: [type: non_null(:integer)] + ], + resolve: fn + %{val: v}, _ -> {:ok, v |> to_string} + args, _ -> {:error, "got #{inspect args}"} + end + + field :thing_by_context, + type: :thing, + resolve: fn + _, %{context: %{thing: id}} -> + {:ok, @db |> OrdMap.get(id)} + _, _ -> + {:error, "No :id context provided"} + end + + field :things, list_of(:thing) do + resolve fn _, _ -> + {:ok, @db |> OrdMap.values |> Enum.sort_by(&OrdMap.get(&1,:id))} + end + end + + field :thing, + type: :thing, + args: [ + id: [ + description: "id of the thing", + type: non_null(:string) + ], + deprecated_arg: [ + description: "This is a deprecated arg", + type: :string, + deprecate: true + + ], + deprecated_non_null_arg: [ + description: "This is a non-null deprecated arg", + type: non_null(:string), + deprecate: true + ], + deprecated_arg_with_reason: [ + description: "This is a deprecated arg with a reason", + type: :string, + deprecate: "reason" + ], + deprecated_non_null_arg_with_reason: [ + description: "This is a non-null deprecated arg with a reasor", + type: non_null(:string), + deprecate: "reason" + ], + ], + resolve: fn + %{id: id}, _ -> + {:ok, @db |> OrdMap.get(id)} + end + + field :deprecated_thing, + type: :thing, + args: [ + id: [ + description: "id of the thing", + type: non_null(:string) + ] + ], + resolve: fn + %{id: id}, _ -> + {:ok, @db |> OrdMap.get(id)} + end, + deprecate: true + + field :deprecated_thing_with_reason, + type: :thing, + args: [ + id: [ + description: "id of the thing", + type: non_null(:string) + ] + ], + deprecate: "use `thing' instead", + resolve: fn + %{id: id}, _ -> + {:ok, @db |> OrdMap.get(id)} + end + end + + input_object :input_thing do + description "A thing as input" + field :value, :integer + field :deprecated_field, :string, deprecate: true + field :deprecated_field_with_reason, :string, deprecate: "reason" + field :deprecated_non_null_field, non_null(:string), deprecate: true + end + + object :thing do + description "A thing" + + field :fail, :id do + @desc "the id we want this field to fail on" + arg :id, :id + + resolve fn + data, %{id: id}, _ -> + if OrdMap.get(data, :id) == id, + do: {:error, "fail"}, + else: {:ok, OrdMap.get(data, :id)} + end + end + + field :id, non_null(:string), + description: "The ID of the thing" + + field :name, :string, + description: "The name of the thing" + + field :value, :integer, + description: "The value of the thing" + + field :other_thing, + type: :thing, + resolve: fn (_, %{source: sourceData}) -> + case OrdMap.get(sourceData, :id) do + "foo" -> {:ok, @db |> OrdMap.get("bar")} + "bar" -> {:ok, @db |> OrdMap.get("foo")} + end + end + + end + +end diff --git a/test/support/support_schemas.ex b/test/support/support_schemas.ex index beacca6d7d..15a5ca0271 100644 --- a/test/support/support_schemas.ex +++ b/test/support/support_schemas.ex @@ -6,7 +6,7 @@ defmodule SupportSchemas do end def load_schema(name) do - Code.require_file("test/support/schemas/#{name}.exs") + Code.load_file("test/support/schemas/#{name}.exs") end @doc """ diff --git a/test/support/things.ex b/test/support/things.ex index 5f363d1230..bd673ed495 100644 --- a/test/support/things.ex +++ b/test/support/things.ex @@ -196,4 +196,4 @@ defmodule Things do end -end +end \ No newline at end of file diff --git a/test/test_helper.exs b/test/test_helper.exs index 2d31f2e42e..bbb8e26402 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,3 +1,7 @@ +# - because of Code.load_file in test\support\support_schemas.ex (Code.require_file blocks tests) +# - possible alternative would be to run tests with async:false and purge modules after their loading +Code.compiler_options(ignore_module_conflict: true) + Code.require_file("test/lib/absinthe/type/fixtures.exs") ExUnit.configure(exclude: [pending: true], timeout: 30_000)