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!