# `Enact.Test`
[🔗](https://github.com/svycal/enact/blob/v0.1.0/lib/enact/test.ex#L1)

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.

# `assert_invalid`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
