Ash Framework: How to test changes to resources?

For example the following test:

    test "stores the actor that updated the record" do
      actor = generate(user(role: :admin))

      artist = generate(artist(name: "First Name"))
      refute artist.updated_by_id == actor.id

      artist = Music.update_artist!(artist, %{name: "Second Name"}, actor: actor)
      assert artist.updated_by_id == actor.id
    end

Looks very reasonable, but will currently fail with

For safety, we prevent any changes after that point because they will bypass validations or other action logic.. To proceed anyway,
you can use force_change_attribute/3. However, you should prefer a pattern like the below, which makes
any custom changes before calling the action.

  Resource
  |> Ash.Changeset.new()
  |> Ash.Changeset.change_attribute(...)
  |> Ash.Changeset.for_create(...)

Which definitely makes sense – prepare all changes prior creating the change set. But how to tight this up with a generator?

Sorry, I’m not following the problem here. Those tests about recording who updated a record shouldn’t fail - they don’t fail in the app at the end of the testing chapter. If you’re seeing failures, can you share the full error with stacktrace?

And what exactly are you looking to update the generators to do?

Thank you for coming back to me. And apologies for potentially unnecessary thread.

I have figured the difference. My lib/tunez/music/changes/update_previous_names.ex looks like:

defmodule Tunez.Music.Changes.UpdatePreviousNames do
  use Ash.Resource.Change

  @impl true
  def change(changeset, _opts, _context) do
    Ash.Changeset.before_action(changeset, fn changeset ->
      new_name = Ash.Changeset.get_attribute(changeset, :name)
      previous_name = Ash.Changeset.get_data(changeset, :name)
      previous_names = Ash.Changeset.get_data(changeset, :previous_names)

      names =
        [previous_name | previous_names]
        |> Enum.uniq()
        |> Enum.reject(fn name -> name == new_name end)

      Ash.Changeset.change_attribute(changeset, :previous_names, names)
    end)
  end
end

While the version from the repo misses being wrapped in Ash.Changeset.before_action/2. Which was introduced on the page 57 (B6).

defmodule Tunez.Music.Changes.UpdatePreviousNames do
  use Ash.Resource.Change

  @impl true
  def change(changeset, _opts, _context) do
    new_name = Ash.Changeset.get_attribute(changeset, :name)
    previous_name = Ash.Changeset.get_data(changeset, :name)
    previous_names = Ash.Changeset.get_data(changeset, :previous_names)

    names =
      [previous_name | previous_names]
      |> Enum.uniq()
      |> Enum.reject(fn name -> name == new_name end)

    Ash.Changeset.change_attribute(changeset, :previous_names, names)
  end
end

Would that be an expected behavior then? I am pretty new to Ash, but using before_action/2 seemed as a good idea :wink:


The full error message:


19:30:59.110 [warning] Changeset has already been validated for action :update.

For safety, we prevent any changes after that point because they will bypass validations or other action logic.. To proceed anyway,
you can use `force_change_attribute/3`. However, you should prefer a pattern like the below, which makes
any custom changes *before* calling the action.

  Resource
  |> Ash.Changeset.new()
  |> Ash.Changeset.change_attribute(...)
  |> Ash.Changeset.for_create(...)

    (elixir 1.18.3) lib/process.ex:896: Process.info/2
    (ash 3.5.21) lib/ash/changeset/changeset.ex:6189: Ash.Changeset.change_attribute/3
    (ash 3.5.21) lib/ash/changeset/changeset.ex:4522: anonymous fn/2 in Ash.Changeset.run_before_actions/1
    (elixir 1.18.3) lib/enum.ex:4968: Enumerable.List.reduce/3
    (elixir 1.18.3) lib/enum.ex:2600: Enum.reduce_while/3
    (ash 3.5.21) lib/ash/changeset/changeset.ex:4497: Ash.Changeset.run_before_actions/1
    (ash 3.5.21) lib/ash/changeset/changeset.ex:4636: Ash.Changeset.run_around_actions/2
    (ash 3.5.21) lib/ash/changeset/changeset.ex:4189: anonymous fn/3 in Ash.Changeset.with_hooks/3
    (ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1400: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
    (db_connection 2.7.0) lib/db_connection.ex:1756: DBConnection.run_transaction/4
    (ash 3.5.21) lib/ash/changeset/changeset.ex:4187: anonymous fn/3 in Ash.Changeset.with_hooks/3
    (ash 3.5.21) lib/ash/changeset/changeset.ex:4331: anonymous fn/2 in Ash.Changeset.transaction_hooks/2
    (ash 3.5.21) lib/ash/changeset/changeset.ex:4174: Ash.Changeset.with_hooks/3
    (ash 3.5.21) lib/ash/actions/update/update.ex:435: Ash.Actions.Update.commit/3
    (ash 3.5.21) lib/ash/actions/update/update.ex:308: Ash.Actions.Update.do_run/4
    (ash 3.5.21) lib/ash/actions/update/update.ex:252: Ash.Actions.Update.run/4
    (ash 3.5.21) lib/ash.ex:3629: Ash.update/3
    (ash 3.5.21) lib/ash.ex:3547: Ash.update!/3
    test/tunez/music/artist_test.exs:117: Tunez.Music.ArtistTest."test Tunez.Music.update_artist/2-3 collects old names when the artist name changes"/1
    (ex_unit 1.18.3) lib/ex_unit/runner.ex:511: ExUnit.Runner.exec_test/2