Testing Ecto Casted Fields

I’m looking at testing if my changeset function only allows the safe fields to be updated.

  def update_changeset(attrs \\ %{}) do
    %Accounts.User{}
    |> cast(attrs, [:safe_field_1, :safe_field_2])
  end

However, looking at the Changeset struct that’s returned by the above function, the casted field information is not stored in the Changeset struct. This makes it not possible to test the whitelisted fields.

This raises two questions for me:

  1. Isn’t it of any value to test if we only whitelist safe fields?
  2. If yes, is there any way to currently do this in Ecto or can this only be done with a change in Ecto core?

Thanks

Any changed fields and their values should be stored in changeset.changes.

1 Like

yes changeset.changes stores the changes. However, it doesn’t solve my needs:

Let’s say I have the following changeset for updating User profile settings and I allow only the :name field to be updated.

  def update_settings_changeset(attrs \\ %{}) do
    %Accounts.User{}
    |> cast(attrs, [:name])
  end

I want to write a test to check if only :name is allowed. changeset.changes allows me to check if I have allowed :name but it’s not suitable for checking if :name ALONE IS allowed

For instance I want to avoid a situation where a developer accidentally adds :role field to the allowed list of fields in the changeset. A test that is checking if :name is allowed will not fail by this addition.

Am I clearer now?

Couldn’t you include a value for ‘:role’ in ‘attrs’ and then test to ensure it is not included in the changeset changes?

If you were updating a user instead of creating a user (as in this example) you would need to make sure that the value of ‘:role’ in ‘attrs’ was different from the existing value. Otherwise it would not be seen as a change and, while the test would pass, it would not be a valid test.

1 Like

Nope. Including :role in attrs wouldn’t work because I’m using :role just to illustrate my point as an example.

While I know the list of fields to be allowed in my changeset, I may not know all the fields that shouldn’t be allowed as fields can be added to the schema at a later point which is not safe to be included in this changeset.

This will build a map that contains the same fields as a schema with the value of each field set to a particular value:

attrs = 
  %Accounts.User{} 
  |> Map.from_struct() 
  |> Map.keys()
  |> Enum.map(&{&1, "*test*"}) 
  |> Enum.into(%{})

You can then pass this in as attrs and all of the fields allowed by cast will either be in changeset.changes or changeset.errors. The fields not allowed by cast will not appear in the changeset as either a change or an error.

1 Like

Good question, I think it’s something we don’t ask often enough! In this case I’d say it’s not worth it because we’re basically testing if Ecto whitelisting works and, yes, it works - it’s tested in Ecto itself :slight_smile:

Aside: A while ago I was working on an “auto” form builder: given ecto schema and whitelist of fields it generates form inputs. A changeset.permitted field would be convenient because I wouldn’t have to pass a separate @permitted assign to the template, but I did the latter and it wasn’t too bad.

What I understood is that @shankardevy was not trying to test that Ecto ‘cast’ was working, but rather that the whitelist had not been accidentally changed.

1 Like

@shankardevy if you want, you can prepare changeset test with empty structure and validate valid status - this enable check changes.

  test "update_changeset/1 enable edit only name" do
     changeset =
        %Accounts.User{}
        |> Accounts.User.update_changeset(%{})

    refute changeset.valid?
    assert [name: {"can't be blank", _validations}] == changeset.errors
  end

@wojtekmach I agree that we shouldn’t try to test things that Ecto is testing internally.

However, my concern here is not testing Ecto, rather a function defined by the user that returns a changeset. i.e., I am not testing cast/3 if it works but a function such as update_user_changeset that uses cast/3. I want to test if my update_user_changeset function is returning a sanitized changeset without allowing any unsafe fields.

Makes sense?

1 Like

Thanks @Ryzey. So far, this is by most the closest thing that I am looking for. I wish there was a cleaner API in Ecto.Changeset itself such as a simple storing of all the whitelisted fields in the changeset struct could be much easier to test.

def changeset(struct, args) do
  struct
  |> cast(args, safe_changeset_attrs())
end

def safe_changeset_attrs(), do: [:foo, :bar]

then in your test case you can just assert on that list. It doesn’t stop you from changing your changeset function completely but I mean, your current test wouldn’t stop someone from using Ecto.Changeset.change directly anyway.

Yes, good one. Only concern is that safe_changeset_attrs is a public function just for the purpose of testing.

1 Like