Does not apply_action/2 don't validate unique constraints?

Hello!

I have to test a unique constraint for a user email and I’m getting just Ecto.ConstraintError but I just got the error only when is used apply_action/2 without it succeeds.

The test case with apply_action/1:

test "when is given an already existing email, returns the error" do
   existing_user = user_fixture()
    invalid_params = %{@valid_params | email: existing_user.email}

    changeset = User.registration_changeset(invalid_params)

    {:ok, user_struct} = Ecto.Changeset.apply_action(changeset, :insert)
    response = Repo.insert(user_struct)

    assert {:error, changeset} = response
end

Receives:

1) test call/1 when is given an already existing email, returns the error (StoneExample.Account.CreateTest)
     test/stone_example/account/create_test.exs:53
     ** (Ecto.ConstraintError) constraint error when attempting to insert struct:
     
         * users_email_index (unique_constraint)
     
     If you would like to stop this constraint violation from raising an
     exception and instead add it as an error to your changeset, please
     call `unique_constraint/3` on your changeset with the constraint
     `:name` as an option.
     
     The changeset has not defined any constraint.

The test case just with Repo.insert/2 without back validations in the application level:

test "when is given an already existing email, returns the error" do
  existing_user = user_fixture()
  invalid_params = %{@valid_params | email: existing_user.email}

  changeset = User.registration_changeset(invalid_params)

  response = Repo.insert(changeset)

  assert {:error, changeset} = response
  assert errors_on(changeset) == %{email: ["Email already exists"]}
end

The test above succeed.

Ecto.Changeset.unique_constraint/2 puts the information about the constraint in the Changeset so Ecto knows to convert a database error into a Changeset error.

apply_action/2 accepts a Changeset and applies its validations to the struct. Ecto necessarily must call the database to check database constraints.

So the answer is to insert/update a Changeset when you have database constraints that need to be checked. Does that make sense?

4 Likes

@mcrumm Makes sense. Thanks for the answer. I’m struggling with apply_action/2 and Repo yet.

The apply_action/2 returns a tuple with a validated struct {ok, struct}. Providing the validated struct to Repo, so Repo hit the database and does not convert the constraints errors into a changeset?

1 Like

Yes, exactly! The info on how to convert the error was in the Changeset - it does not get carried over I to the struct. Repo needs a Changeset to perform the insert, so when you provide a struct it will just wrap the struct in a bare Changeset under-the-hood. That’s why you see this message in the error:

The changeset has not defined any constraint.

By the way, welcome to the community @allansduarte!

2 Likes

Thanks for clarifying me. Now the test works :slight_smile:

1 Like