Skip to content

Commit

Permalink
chore(Ash.Test): Updates to assert_has_error and `refute_has_erro…
Browse files Browse the repository at this point in the history
…r` to accept `:ok` tuples (#1706)

* chore(`Ash.Test`): Allow `:ok` tuples to be passed to `assert_has_error` without error

This case will always have a failing assertion because there are no errors at all

* chore(`Ash.Test`): Allow `:ok`/`:error` tuples to be passed to `refute_has_error` without error

* chore(`Ash.Test`): Add tests for changes to `assert_has_error` and `refute_has_error`

* chore(`Ash.Test`): Deprecate the `error_class` argument to `Ash.Test.refute_has_error`

It's confusing when combined with the required `callback` argument and the fact that
this is a refutation, not an assertion - too many negatives going around
  • Loading branch information
sevenseacat authored Jan 11, 2025
1 parent c59eb2e commit b468bcb
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 19 deletions.
74 changes: 58 additions & 16 deletions lib/ash/test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ defmodule Ash.Test do
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, term},
Ash.Changeset.t()
| Ash.Query.t()
| Ash.ActionInput.t()
| {:error, term}
| {:ok, term}
| :ok,
error_class :: Ash.Error.class_module(),
(Ash.Error.t() -> boolean)
) :: Ash.Error.t() | no_return
Expand Down Expand Up @@ -44,6 +49,9 @@ defmodule Ash.Test do
match
end

def assert_has_error({:ok, _record}, error_class, _callback, _opts), do: no_errors(error_class)
def assert_has_error(:ok, error_class, _callback, _opts), do: no_errors(error_class)

def assert_has_error(changeset_query_or_input, error_class, callback, opts) do
type =
case changeset_query_or_input do
Expand Down Expand Up @@ -78,33 +86,67 @@ defmodule Ash.Test do
match
end

defp no_errors(error_class) do
message =
if error_class do
"Expected the value to have errors of class #{inspect(error_class)}, but it had no errors"
else
"Expected the value to have errors matching the provided callback, but it had no errors"
end

ExUnit.Assertions.flunk(message)
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.
The `error_class` argument has been deprecated and should not be used.
"""
@spec refute_has_error(
Ash.Changeset.t() | Ash.Query.t() | Ash.ActionInput.t(),
Ash.Changeset.t()
| Ash.Query.t()
| Ash.ActionInput.t()
| :ok
| {:ok, term}
| {:error, term},
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
def refute_has_error(changeset_query_or_input, error_class \\ nil, callback, opts \\ [])

error = Ash.Error.to_error_class(changeset_query_or_input)
# An :ok response doesn't have any errors!
def refute_has_error(:ok, _error_class, _callback, _opts), do: :ok
def refute_has_error({:ok, _record}, _error_class, _callback, _opts), do: :ok

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__)}"
)
def refute_has_error({:error, error}, error_class, callback, opts) do
if error_class != nil do
IO.warn("`error_class` argument to `refute_has_error` is deprecated and will be ignored")
end

error = Ash.Error.to_error_class(error)
match = Enum.find(error.errors, callback)

ExUnit.Assertions.assert(!match,
message:
opts[:message] ||
"""
Expected no errors to match the provided callback, but one did.
Errors:
#{inspect(match, pretty: true)}
"""
)

match
end

def refute_has_error(changeset_query_or_input, error_class, callback, opts) do
if error_class != nil do
IO.warn("`error_class` argument to `refute_has_error` is deprecated and will be ignored")
end

error = Ash.Error.to_error_class(changeset_query_or_input)
match = Enum.find(error.errors, callback)

ExUnit.Assertions.refute(match,
Expand Down
124 changes: 121 additions & 3 deletions test/error_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ defmodule Ash.Test.ErrorTest do
actions do
default_accept :*
defaults [:read, :destroy, create: :*, update: :*]

create :create_with_error do
validate attribute_equals(:id, false)
end
end

attributes do
Expand Down Expand Up @@ -194,9 +198,7 @@ defmodule Ash.Test.ErrorTest do
err.error == "whoops!"
end)

Ash.Test.refute_has_error(cs, Ash.Error.Unknown, fn err ->
err.error == "yay!"
end)
Ash.Test.refute_has_error(cs, fn err -> err.error == "yay!" end)

assert clean(Ash.Error.to_error_class(cs)) ==
clean(Ash.Error.to_error_class([error1, error2], changeset: cs))
Expand Down Expand Up @@ -233,6 +235,122 @@ defmodule Ash.Test.ErrorTest do
end
end

describe "assert_has_error" do
test "raises if the value is :ok" do
assert_raise ExUnit.AssertionError, ~r/it had no errors/, fn ->
Ash.Test.assert_has_error(:ok, Ash.Error.Invalid, fn _err -> true end)
end
end

test "raises if the value is an :ok tuple" do
assert_raise ExUnit.AssertionError, ~r/it had no errors/, fn ->
Ash.Test.assert_has_error(
{:ok, :something_successful},
Ash.Error.Invalid,
fn _err -> true end
)
end
end

test "raises if the value doesn't have any errors of the expected type" do
changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
error = Ash.create(changeset)

assert_raise ExUnit.AssertionError,
~r/Expected the changeset to have errors of class Ash.Error.Unknown/,
fn ->
Ash.Test.assert_has_error(changeset, Ash.Error.Unknown, fn _err -> true end)
end

assert_raise ExUnit.AssertionError,
~r/Expected the value to have errors of class Ash.Error.Unknown/,
fn ->
Ash.Test.assert_has_error(error, Ash.Error.Unknown, fn _err -> true end)
end
end

test "raises if the value has an error of the expected type but doesn't pass the callback" do
changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
error = Ash.create(changeset)

assert_raise ExUnit.AssertionError,
~r/Expected at least one error to match the provided callback/,
fn ->
Ash.Test.assert_has_error(changeset, Ash.Error.Invalid, fn _err -> false end)
end

assert_raise ExUnit.AssertionError,
~r/Expected at least one error to match the provided callback/,
fn ->
Ash.Test.assert_has_error(error, Ash.Error.Invalid, fn _err -> false end)
end
end

test "raises if the value doesn't pass the callback" do
changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
error = Ash.create(changeset)

assert_raise ExUnit.AssertionError,
~r/Expected at least one error to match the provided callback/,
fn -> Ash.Test.assert_has_error(changeset, fn _err -> false end) end

assert_raise ExUnit.AssertionError,
~r/Expected at least one error to match the provided callback/,
fn -> Ash.Test.assert_has_error(error, fn _err -> false end) end
end

test "passes if the value matches the type and passes the callback" do
changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
error = Ash.create(changeset)

assert Ash.Test.assert_has_error(changeset, Ash.Error.Invalid, fn _err -> true end)
assert Ash.Test.assert_has_error(error, Ash.Error.Invalid, fn _err -> true end)
end

test "passes if the value passes the callback" do
changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
error = Ash.create(changeset)

assert Ash.Test.assert_has_error(changeset, fn _err -> true end)
assert Ash.Test.assert_has_error(error, fn _err -> true end)
end
end

describe "refute_has_error" do
test "passes if the value is :ok" do
Ash.Test.refute_has_error(:ok, fn _err -> true end)
end

test "passes if the value is an :ok tuple" do
Ash.Test.refute_has_error({:ok, :something_successful}, fn _err -> true end)
end

test "passes if the value doesn't pass the callback" do
changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
error = Ash.create(changeset)

Ash.Test.refute_has_error(changeset, fn _err -> false end)
Ash.Test.refute_has_error(error, fn _err -> false end)
end

test "raises if the value passes the callback" do
changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
error = Ash.create(changeset)

assert_raise ExUnit.AssertionError,
~r/Expected no errors to match the provided callback/,
fn ->
Ash.Test.refute_has_error(changeset, fn _err -> true end)
end

assert_raise ExUnit.AssertionError,
~r/Expected no errors to match the provided callback/,
fn ->
Ash.Test.refute_has_error(error, fn _err -> true end)
end
end
end

defp same_elements?(xs, ys) when is_list(xs) and is_list(ys) do
Enum.sort(clean(xs)) == Enum.sort(clean(ys))
end
Expand Down

0 comments on commit b468bcb

Please sign in to comment.