Enact.Test (Enact v0.1.0)

Copy Markdown View Source

Shared test helpers for host-app action tests (and Enact's own suite).

import Enact.Test

test "rejects a blank name" do
  {:error, _} = result = Enact.run(CreateProject, %{"name" => ""}, actor: actor)
  assert_invalid(result, on: :name)
end

Intended for the test environment; the assertions require ExUnit.

Summary

Functions

Asserts the result is {:error, %Enact.Error{type: :invalid}}, and — given on: field — that the changeset carries an error on that top-level field. Returns the changeset for further assertions.

Asserts that an input module rejects "" on every non-string castable field for the given mode.

Builds an Enact.Context for unit-testing validate/2 callbacks or fetchers directly, without running the pipeline.

Flattens a changeset's errors into a map of field → messages, with nested embed errors as per-index lists of maps — the conventional errors_on helper, shipped so host apps don't reinvent it.

Functions

assert_invalid(result, opts \\ [])

@spec assert_invalid(
  term(),
  keyword()
) :: Ecto.Changeset.t()

Asserts the result is {:error, %Enact.Error{type: :invalid}}, and — given on: field — that the changeset carries an error on that top-level field. Returns the changeset for further assertions.

assert_rejects_empty_strings(module, mode, opts \\ [])

@spec assert_rejects_empty_strings(module(), atom(), keyword()) :: :ok

Asserts that an input module rejects "" on every non-string castable field for the given mode.

Ecto's default cast silently coerces "" to nil on any field type. For non-string fields that turns malformed input into a null-clear instruction instead of a cast error. This probe calls module.changeset/3 with "" for each non-string scalar field in fields(mode) and fails unless the field carries a cast error — regardless of whether the module uses Enact.InputSchema.cast_input/4 or a stock cast with empty_values: [].

Probing is safe because input modules are dependency-free by contract (no ctx, no repo). Errors on other fields (for example validate_required) are ignored; only the probed field's cast outcome is checked. :string, :binary, and :binary_id fields are skipped: "" is a valid value for the first two, and :binary_id's changeset-level cast accepts any binary (its format is validated at dump time). A rejection counts when the probed field carries any error other than validation: :required — custom types may tag rejections with their own metadata (Ecto.Enum uses :inclusion), while a :required error is the footgun's own signature: "" silently coerced to nil, then caught downstream.

Coverage is top-level only: item schemas have no fields/1 manifest, so their cast lists are not introspectable. Item-level strictness is a convention — cast item fields with cast_input/4 — rather than a probed guarantee.

Options: :except — fields whose custom types accept "" deliberately.

build_ctx(opts \\ [])

@spec build_ctx(keyword()) :: Enact.Context.t()

Builds an Enact.Context for unit-testing validate/2 callbacks or fetchers directly, without running the pipeline.

Options (all optional): :actor (default :test_actor), :subject, :params (default %{}), :repo, :mode (default :create), :assigns (default %{}).

:params are stored as given — this helper does not stringify keys. Enact.run/3 / dry_run/3 / subject/3 / authorized/3 do. Pass string keys here if the code under test pattern-matches on them.

errors_on(changeset)

@spec errors_on(Ecto.Changeset.t()) :: map()

Flattens a changeset's errors into a map of field → messages, with nested embed errors as per-index lists of maps — the conventional errors_on helper, shipped so host apps don't reinvent it.

assert %{milestones: [%{}, %{owner_id: ["not found"]}]} = errors_on(changeset)