Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/ash_jido/error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ defmodule AshJido.Error do
@spec extract_underlying_errors(Exception.t()) :: [Exception.t()]
def extract_underlying_errors(ash_error) do
cond do
Map.has_key?(ash_error, :errors) and is_list(ash_error.errors) ->
Map.has_key?(ash_error, :errors) and is_list(ash_error.errors) and ash_error.errors != [] ->
ash_error.errors

Map.has_key?(ash_error, :error) ->
Expand Down
7 changes: 4 additions & 3 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ defmodule AshJido.MixProject do
[
# Runtime dependencies
{:ash, "~> 3.12"},
{:jido, "~> 2.0.0-rc"},
{:jido_action, "~> 2.0.0-rc"},
{:jido_signal, "~> 2.0.0-rc"},
{:jido, "~> 2.1"},
{:jido_action, "~> 2.1"},
{:jido_signal, "~> 2.1"},
{:libgraph, "~> 0.16.0", override: true},
{:splode, "~> 0.3"},
{:zoi, "~> 0.14"},

Expand Down
254 changes: 254 additions & 0 deletions test/ash_jido/error_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
defmodule AshJido.ErrorTest do
use ExUnit.Case, async: true

alias AshJido.Error

describe "from_ash/1" do
test "converts Ash.Error.Invalid to validation_error" do
ash_error = %Ash.Error.Invalid{
errors: [
%Ash.Error.Changeset.Required{field: :name, message: "is required"}
]
}

result = Error.from_ash(ash_error)

assert %Jido.Action.Error.InvalidInputError{} = result
assert result.message =~ "is required"
assert result.details.ash_error == ash_error
assert result.details.fields == %{name: ["is required"]}
end

test "converts Ash.Error.Forbidden to execution_error with forbidden reason" do
ash_error = %Ash.Error.Forbidden{
errors: [
%Ash.Error.Forbidden.Policy{message: "not authorized"}
]
}

result = Error.from_ash(ash_error)

assert %Jido.Action.Error.ExecutionFailureError{} = result
assert result.details.reason == :forbidden
assert result.details.ash_error == ash_error
end

test "converts Ash.Error.Framework to internal_error" do
ash_error = %Ash.Error.Framework{
errors: [
%Ash.Error.Framework.InvalidReturn{message: "invalid return"}
]
}

result = Error.from_ash(ash_error)

assert %Jido.Action.Error.InternalError{} = result
assert result.details.ash_error == ash_error
end

test "converts Ash.Error.Unknown to internal_error" do
ash_error = %Ash.Error.Unknown{
errors: [
%Ash.Error.Unknown.UnknownError{error: "something went wrong"}
]
}

result = Error.from_ash(ash_error)

assert %Jido.Action.Error.InternalError{} = result
assert result.details.ash_error == ash_error
end

test "converts generic exception to execution_error" do
ash_error = %RuntimeError{message: "runtime error"}

result = Error.from_ash(ash_error)

assert %Jido.Action.Error.ExecutionFailureError{} = result
assert result.details.ash_error == ash_error
end

test "preserves underlying errors in details" do
underlying = [%Ash.Error.Changeset.Required{field: :email, message: "is required"}]
ash_error = %Ash.Error.Invalid{errors: underlying}

result = Error.from_ash(ash_error)

assert length(result.details.underlying_errors) == 1
assert hd(result.details.underlying_errors).__struct__ == Ash.Error.Changeset.Required
end
end

describe "extract_underlying_errors/1" do
test "extracts errors list from Ash error" do
errors = [
%Ash.Error.Changeset.Required{field: :name, message: "is required"},
%Ash.Error.Changeset.Required{field: :email, message: "is required"}
]

ash_error = %Ash.Error.Invalid{errors: errors}

result = Error.extract_underlying_errors(ash_error)

assert length(result) == 2
end

test "extracts single error from error field" do
inner_error = %Ash.Error.Changeset.Required{field: :name, message: "is required"}
ash_error = %Ash.Error.Unknown{error: inner_error, errors: []}

result = Error.extract_underlying_errors(ash_error)

assert length(result) == 1
assert hd(result) == inner_error
end

test "returns empty list when no errors present" do
ash_error = %Ash.Error.Invalid{errors: []}

result = Error.extract_underlying_errors(ash_error)

assert result == []
end

test "returns empty list for exception without errors field" do
exception = %RuntimeError{message: "test"}

result = Error.extract_underlying_errors(exception)

assert result == []
end
end

describe "extract_field_errors/1" do
test "extracts field errors from changeset errors" do
ash_error = %Ash.Error.Invalid{
errors: [
%Ash.Error.Changeset.Required{field: :name, message: "is required"},
%Ash.Error.Changeset.InvalidAttribute{field: :email, message: "is invalid"}
]
}

result = Error.extract_field_errors(ash_error)

assert result.name == ["is required"]
assert result.email == ["is invalid"]
end

test "extracts field errors from path-based errors" do
ash_error = %Ash.Error.Invalid{
errors: [
%Ash.Error.Changeset.InvalidAttribute{
path: [:user, :profile, :name],
message: "is too short"
}
]
}

result = Error.extract_field_errors(ash_error)

assert result.name == ["is too short"]
end

test "groups multiple errors per field" do
ash_error = %Ash.Error.Invalid{
errors: [
%Ash.Error.Changeset.Required{field: :name, message: "is required"},
%Ash.Error.Changeset.InvalidAttribute{field: :name, message: "is too short"}
]
}

result = Error.extract_field_errors(ash_error)

assert result.name == ["is required", "is too short"]
end

test "returns empty map for errors without field information" do
ash_error = %Ash.Error.Invalid{
errors: [
%Ash.Error.Framework.InvalidReturn{message: "invalid"}
]
}

result = Error.extract_field_errors(ash_error)

assert result == %{}
end
end

describe "extract_changeset_errors/1" do
test "extracts changeset-related errors" do
ash_error = %Ash.Error.Invalid{
errors: [
%Ash.Error.Changeset.Required{field: :name, message: "is required"},
%Ash.Error.Changeset.InvalidAttribute{field: :email, message: "is invalid"},
%Ash.Error.Framework.InvalidReturn{message: "framework error"}
]
}

result = Error.extract_changeset_errors(ash_error)

assert length(result) == 2

changeset_modules = Enum.map(result, & &1.type)
assert Ash.Error.Changeset.Required in changeset_modules
assert Ash.Error.Changeset.InvalidAttribute in changeset_modules
end

test "extracts validation-related errors" do
ash_error = %Ash.Error.Invalid{
errors: [
%Ash.Error.Validation.InvalidAttribute{field: :age, message: "must be positive"}
]
}

result = Error.extract_changeset_errors(ash_error)

assert length(result) == 1
assert hd(result).type == Ash.Error.Validation.InvalidAttribute
end

test "includes error details in result" do
ash_error = %Ash.Error.Invalid{
errors: [
%Ash.Error.Changeset.Required{field: :name, message: "is required"}
]
}

result = Error.extract_changeset_errors(ash_error)

assert hd(result).message =~ "is required"
assert is_map(hd(result).details)
end

test "returns empty list when no changeset errors" do
ash_error = %Ash.Error.Invalid{
errors: [
%Ash.Error.Framework.InvalidReturn{message: "framework error"}
]
}

result = Error.extract_changeset_errors(ash_error)

assert result == []
end
end

describe "build_details/1" do
test "builds complete details map with all error information" do
ash_error = %Ash.Error.Invalid{
errors: [
%Ash.Error.Changeset.Required{field: :name, message: "is required"},
%Ash.Error.Changeset.InvalidAttribute{field: :email, message: "is invalid"}
]
}

result = Error.from_ash(ash_error)

assert result.details.ash_error == ash_error
assert length(result.details.underlying_errors) == 2
assert map_size(result.details.fields) == 2
assert length(result.details.changeset_errors) == 2
end
end
end
Loading