ash/lib/ash/test.ex
2023-10-21 23:30:33 -04:00

140 lines
3.9 KiB
Elixir

defmodule Ash.Test do
@moduledoc """
Testing helpers for Ash.
"""
require ExUnit.Assertions
@doc """
Assert that the given changeset, query, or action input has a matching error.
Use the optional second argument to assert that the errors (all together) are of a specific class.
"""
@spec assert_has_error(
Ash.Changeset.t() | Ash.Query.t() | Ash.ActionInput.t(),
error_class :: Ash.Error.class_module(),
(Ash.Error.t() -> boolean)
) :: Ash.Error.t() | no_return
def assert_has_error(changeset_query_or_input, error_class \\ nil, callback, opts \\ []) do
type =
case changeset_query_or_input do
%Ash.Changeset{} -> "changeset"
%Ash.Query{} -> "query"
%Ash.ActionInput{} -> "action input"
end
error = Ash.Error.to_error_class(changeset_query_or_input)
if error_class do
ExUnit.Assertions.assert(error.__struct__ == error_class,
message:
"Expected the #{type} to have errors of class #{inspect(error_class)}, got: #{inspect(error.__struct__)}"
)
end
match = Enum.find(error.errors, callback)
ExUnit.Assertions.assert(match,
message:
opts[:message] ||
"""
Expected at least one error to match the provided callback, but none did.
Errors:
#{inspect(error.errors, pretty: true)}
"""
)
match
end
@doc """
Refute that the given changeset, query, or action input has a matching error.
Use the optional second argument to assert that the errors (all together) are of a specific class.
"""
@spec refute_has_error(
Ash.Changeset.t() | Ash.Query.t() | Ash.ActionInput.t(),
error_class :: Ash.Error.class_module(),
(Ash.Error.t() -> boolean)
) :: Ash.Error.t() | no_return
def refute_has_error(changeset_query_or_input, error_class \\ nil, callback, opts \\ []) do
type =
case changeset_query_or_input do
%Ash.Changeset{} -> "changeset"
%Ash.Query{} -> "query"
%Ash.ActionInput{} -> "action input"
end
error = Ash.Error.to_error_class(changeset_query_or_input)
if error_class do
ExUnit.Assertions.assert(error.__struct__ == error_class,
message:
"Expected the #{type} to have errors of class #{inspect(error_class)}, got: #{inspect(error.__struct__)}"
)
end
match = Enum.find(error.errors, callback)
ExUnit.Assertions.refute(match,
message:
opts[:message] ||
"""
Expected no errors to match the provided callback, but one did.
Matching Error:
#{inspect(match, pretty: true)}
Errors:
#{inspect(error.errors, pretty: true)}
"""
)
match
end
@doc """
Clears the `__metadata__` field and the underlying ecto `__meta__` field
This allows for easier comparison of changeset/query results
"""
def strip_metadata(structs) when is_list(structs), do: Enum.map(structs, &strip_metadata/1)
def strip_metadata(tuple) when is_tuple(tuple) do
tuple
|> Tuple.to_list()
|> strip_metadata()
|> List.to_tuple()
end
def strip_metadata(%page_struct{results: results} = page)
when page_struct in [Ash.Page.Offset, Ash.Page.Keyset] do
%{page | results: Enum.map(results, &strip_metadata/1)}
end
def strip_metadata(%{__metadata__: _, __meta__: _} = struct) do
struct = %{struct | __metadata__: %{}, __meta__: %Ecto.Schema.Metadata{}}
struct
|> Map.keys()
|> Enum.reduce(struct, fn key, struct ->
Map.update!(struct, key, &strip_metadata/1)
end)
end
def strip_metadata(%{__metadata__: _} = struct) do
struct = %{struct | __metadata__: %{}}
struct
|> Map.keys()
|> Enum.reduce(struct, fn key, struct ->
Map.update!(struct, key, &strip_metadata/1)
end)
end
def strip_metadata(other), do: other
end