How to properly test User schema with custom_fields and pow_user_fields

Hey dudes, how are you?

I keep doing an application to learn more about Phoenix and I’m using Pow as a lib to manage user session and registration step.

My problem is that I created a schema/table with pow_user_fields() and also add some custom fields by my own to the schema/table.

Here is my current code:

schemas/user.ex

defmodule Conversations.Users.Schemas.User do
  use Ecto.Schema
  use Pow.Ecto.Schema
  import Ecto.Changeset

  @email_validation_regex ~r/\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
  @permitted_params [:name, :email, :role]

  schema "users" do
    pow_user_fields()

    field :name, :string
    field :role, :string, default: "teacher"
    field :namespace, :string

    timestamps()
  end

  @doc false
  def changeset(_user, attrs \\ %{})

  @doc false
  def changeset(%{role: role} = user, attrs) when role == "teacher" do
    user
    |> pow_changeset(attrs)
    |> cast(attrs, @permitted_params ++ [:namespace])
    |> validate_required(:namespace)
    |> validate_length(:namespace, max: 120)
    |> slugify_namespace()
    |> unique_constraint(:namespace)
    |> validate()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> pow_changeset(attrs)
    |> cast(attrs, @permitted_params)
    |> validate()
  end

  defp validate(user) do
    user
    |> validate_required(@permitted_params)
    |> validate_length(:name, max: 120)
    |> validate_format(:email, @email_validation_regex)
    |> unique_constraint(:email, name: :users_email_index)
    |> validate_inclusion(:role, ~w(student admin teacher))
  end

  defp slugify_namespace(%{changes: %{namespace: namespace} = changes} = user_changeset)
       when map_size(changes) > 0
       when not is_nil(namespace)
       when namespace != "" do
    slugified_namespace =
      namespace
      |> String.downcase()
      |> String.replace(~r/[^a-z0-9\s-]/, "")
      |> String.replace(~r/(\s|-)+/, "-")

    put_change(user_changeset, :namespace, slugified_namespace)
  end

  defp slugify_namespace(changeset), do: changeset
end

schemas/user_test.exs

defmodule Conversations.Users.Schemas.UserTest do
  use Conversations.DataCase

  alias Conversations.Factory
  alias Conversations.Users.Schemas.User
  alias Conversations.Repo

  def student_factory(attrs \\ %{}) do
    Factory.build(:user, Map.merge(attrs, %{role: "student"}))
  end

  def teacher_factory(attrs \\ %{}) do
    Factory.build(:user, attrs)
  end

  test "return valid true when data is valid" do
    changeset =
      student_factory()
      |> User.changeset(%{})

    assert changeset.valid?
  end

  describe "user schema validations" do
    test "name must not be blank" do
      changeset =
        student_factory()
        |> User.changeset(%{name: ""})

      refute changeset.valid?
      assert "can't be blank" in errors_on(changeset).name
    end

    test "name must not be nil" do
      changeset =
        student_factory()
        |> User.changeset(%{name: nil})

      refute changeset.valid?
      assert "can't be blank" in errors_on(changeset).name
    end

    test "name must not be higher than 120 chars" do
      changeset =
        student_factory()
        |> User.changeset(%{name: String.duplicate("a", 121)})

      refute changeset.valid?
      assert "should be at most 120 character(s)" in errors_on(changeset).name
    end

    test "email must not be blank" do
      changeset =
        student_factory()
        |> User.changeset(%{email: ""})

      refute changeset.valid?
      assert "can't be blank" in errors_on(changeset).email
    end

    test "email must not be nil" do
      changeset =
        student_factory()
        |> User.changeset(%{email: nil})

      refute changeset.valid?
      assert "can't be blank" in errors_on(changeset).email
    end

    test "email must not be a invalid format" do
      changeset =
        student_factory()
        |> User.changeset(%{email: "email@com"})

      refute changeset.valid?
      assert "has invalid format" in errors_on(changeset).email
    end

    test "email must be unique" do
      schema = Factory.insert(:user, %{name: "name", email: "email@example.org"})
      other_schema = student_factory(%{email: schema.email})

      {:error, other_changeset} =
        other_schema
        |> User.changeset(%{})
        |> Repo.insert()

      assert "has already been taken" in errors_on(other_changeset).email

      {:ok, _} =
        student_factory(%{email: "other@email.com"})
        |> User.changeset(%{})
        |> Repo.insert()
    end

    test "role must not be nil" do
      changeset =
        student_factory()
        |> User.changeset(%{role: nil})

      refute changeset.valid?
      assert "can't be blank" in errors_on(changeset).role
    end

    test "role must not be invalid" do
      changeset =
        student_factory()
        |> User.changeset(%{role: "xunda"})

      refute changeset.valid?
      assert "is invalid" in errors_on(changeset).role

      changeset =
        student_factory()
        |> User.changeset(%{role: "teacher"})

      assert changeset.valid?

      changeset =
        student_factory()
        |> User.changeset(%{role: "student"})

      assert changeset.valid?

      changeset =
        student_factory()
        |> User.changeset(%{role: "admin"})

      assert changeset.valid?
    end

    test "default role must be student" do
      user = student_factory()
      changeset = User.changeset(user, %{role: ""})

      assert changeset.valid?
      assert user.role == "student"
    end

    test "namespace is required when role is teacher" do
      changeset =
        teacher_factory()
        |> User.changeset(%{namespace: ""})

      refute changeset.valid?
      assert "can't be blank" in errors_on(changeset).namespace
    end

    test "namespace must not be higher than 120 chars" do
      changeset =
        teacher_factory()
        |> User.changeset(%{namespace: String.duplicate("a", 121)})

      refute changeset.valid?
      assert "should be at most 120 character(s)" in errors_on(changeset).namespace
    end

    test "slugify namespace" do
      changeset =
        teacher_factory()
        |> User.changeset(%{namespace: "my name Space"})

      assert Ecto.Changeset.get_field(changeset, :namespace) == "my-name-space"

      changeset =
        teacher_factory()
        |> User.changeset(%{namespace: "my name    otherSPACE"})

      assert Ecto.Changeset.get_field(changeset, :namespace) == "my-name-otherspace"

      changeset =
        teacher_factory()
        |> User.changeset(%{namespace: "1-my xpa c"})

      assert Ecto.Changeset.get_field(changeset, :namespace) == "1-my-xpa-c"
    end

    test "namespace slug must be unique" do
      schema = Factory.insert(:user, %{role: "teacher", namespace: "my namespace", email: "name@email.com"})
      other_schema = teacher_factory(%{namespace: schema.namespace})

      {:error, other_changeset} =
        other_schema
        |> User.changeset(%{})
        |> Repo.insert()

      assert "has already been taken" in errors_on(other_changeset).namespace

      {:ok, _} =
        teacher_factory(%{namespace: "test namespace"})
        |> User.changeset(%{})
        |> Repo.insert()
    end
  end
end

And I have a factory built with ex_machina:

factory.ex

defmodule Conversations.Factory do
  use ExMachina.Ecto, repo: Conversations.Repo

  alias Conversations.Users.Schemas.User

  def user_factory do
    %User{
      name: "Name",
      email: "name@example.org"
    }
  end
end

With this code setup I’m able to register a new user in the registration/new and also to log in into system with sessions/new.

But when I ran my tests I got this errors:

1) test user schema validations namespace slug must be unique (Conversations.Users.Schemas.UserTest)
     test/conversations/users/schemas/user_test.exs:178
     ** (KeyError) key :namespace not found in: %{password: ["can't be blank"]}
     code: assert "has already been taken" in errors_on(other_changeset).namespace
     stacktrace:
       test/conversations/users/schemas/user_test.exs:187: (test)

10) test call/1 with valid data create a new admin (Conversations.Users.UseCases.Admins.CreateTest)
     test/conversations/users/use_cases/admins/create_test.exs:8
     ** (MatchError) no match of right hand side value: {:error, #Ecto.Changeset<action: :insert, changes: %{email: "email@example.com", name: "Cezer"}, errors: [password: {"can't be blank", [validation: :required]}], data: #Conversations.Users.Schemas.User<>, valid?: false>}
     code: {:ok, user} = Create.call(params)
     stacktrace:
       test/conversations/users/use_cases/admins/create_test.exs:11: (test)

Ok, seems that the password is blank, so I add it to my factory.ex:

defmodule Conversations.Factory do
  use ExMachina.Ecto, repo: Conversations.Repo

  alias Conversations.Users.Schemas.User

  def user_factory do
    %User{
      name: "Name",
      email: "name@example.org",
      password: "123qwe123"
    }
  end
end

And when I ran my tests:

  1) test user schema validations namespace slug must be unique (Conversations.Users.Schemas.UserTest)
     test/conversations/users/schemas/user_test.exs:178
     ** (KeyError) key :namespace not found in: %{password_hash: ["can't be blank"]}
     code: assert "has already been taken" in errors_on(other_changeset).namespace
     stacktrace:
       test/conversations/users/schemas/user_test.exs:187: (test)

Now seem that the password_hash needs to be populated, and here I go, factory.ex:

defmodule Conversations.Factory do
  use ExMachina.Ecto, repo: Conversations.Repo

  alias Conversations.Users.Schemas.User

  def user_factory do
    %User{
      name: "Name",
      email: "name@example.org",
      password: "123qwe123",
      password_hash: "$pbkdf2-sha512$100000$ppqkotA6RXjLBSpPPKqHTg==$zIkESOwi5cempseAA3PoxZRpIGGP4c5x4/y5L5M/6YUjhVKS1yPXJ9134sPZ9ZLPMjz3tEDgSb/cX6ukHjoFNg=="
    }
  end
end

And running my tests again:

  2) test user schema validations email must be unique (Conversations.Users.Schemas.UserTest)
     test/conversations/users/schemas/user_test.exs:79
     ** (KeyError) key :email not found in: %{current_password: ["can't be blank"]}
     code: assert "has already been taken" in errors_on(other_changeset).email
     stacktrace:
       test/conversations/users/schemas/user_test.exs:88: (test)

Now the error seems to be in the current_password field.

If I add some data to it in my factory the error will keep existing.

What I’m doing wrong? Should I add those fields to the @permitted_params into my schema? I’m missing something? Is there another way to test a user schema with custom and pow fields?

So my problem/question is, how to properly test a user schema that uses pow_fields and some custom fields? The app is running fine my the tests not.

Sorry for the long post, I tried to be more clear as possible.

Thanks!

You want to make sure, that, you are casting it properly, and not set it in the struct.

    test "namespace is required when role is teacher" do
      changeset =
        teacher_factory()
        |> User.changeset(%{namespace: "", email: "teacher@exmaple.com", password: "testtest", password_confirmation: "testtest"})

      refute changeset.valid?
      assert "can't be blank" in errors_on(changeset).namespace
    end