-
Notifications
You must be signed in to change notification settings - Fork 146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How do you handle password hashing? #82
Comments
@aaronrenner That is a great question. There is a proposed PR for using changesets when creating factories (#78), but I'm thinking that may add more complexity than is necessary. Here's what I've done: def factory(:user) do
%User{
email: Sequence.next(:email, &"email-#{&1}@example.com"),
encrypted_password: "something",
}
end
def set_password(user, password) do
hashed_password = Comeonin.Bcrypt.hashpwsalt(password)
%{user | encrypted_password: hashed_password}
end
# In a test
test "signs in the user if information is correct", %{conn: conn} do
user = build(:user, email: "foo@example.com") |> set_password("pass") |> create
conn = post conn, session_path(conn, :create), user: %{email: "foo@example.com", password: "pass"}
assert signed_in?(conn, user) # Test that the conn returns the right status code, sets assigns or whatever
end So I only use the set_password function when I really need to set the password. Generally this is only for a couple tests where I test the actual sign in implementation. The rest would set the user on the conn and bypass the sign in controller. So for this to work it assumes:
For example if you do something like this: post(sign_in_path(conn, :create), username: "me", pass: "pass")
|> get(thing_path(conn, :index)) Then it won't work since you're actually hitting the real sign in controller for all your tests. Instead you could do something more like this: test "list things" do
user = create(:user)
# You'd probably extract this to a `sign_in` function or do it in the `setup` block
conn = conn() |> Plug.Conn.assign(:user, user)
things = create_pair(:things)
# Works because the conn already has a user
conn = get conn, experiment_path(conn, :index)
for thing <- things do
assert html_response(conn, 200) =~ thing.title
end
end
# Auth plug
# If the conn already has a user, assume they're signed in
def authenticate(conn, _opts) do
if already_signed_in?(conn) do
conn
else
get_and_assign_user
end
end
defp already_signed_in?(conn) do
!!conn.assigns.user
end I recommend this anyway because it makes tests simpler and faster since you don't have to hit the sign in controller every time. LMK if that answers your question. It may have been too much, but if someone else finds this on google I'd rather have a more complete answer that they can use :) |
Thanks @paulcsmith for a great response. The only potential drawback I see with adding I really appreciate seeing how you handle this, and I don't have a better solution at the moment. I'll keep pondering it and let you know if I come up with an alternative solution. |
I'm glad it helped. That is a really good point! One thing you could do is make the password hashing function public in your model and call that from within the factory. Just delegate it. Haven't tried it myself, but it might work well. Let me know if you figure something else out :)
|
@paulcsmith After thinking about this for a while, here's what I came up with... My def set_password(user, password) do
user
|> User.changeset(%{"password" =>password})
|> Ecto.Changeset.apply_changes()
end |
@aaronrenner Ah that's interesting! Thanks for posting for others to check it out as well. I'll give this a try when I need it again :) |
@aaronrenner thanks for that tip. I had to change apply_changes to update: def set_password(user, password) do
user
|> User.changeset(%{"password" =>password})
|> Repo.update!
end |
@dustinfarris I don't think you need to use build(:user) |> set_password("secret") |> insert |
Ah, ok. That works better. Thanks!
Dustin Farris |
For anyone else who uses this thread to get password encryption working. Here's what I've done. if using coherance 0.3.0, ecto 2.0.5, phoenix 1.2.1. I had to do the following. In factory
Note: In test file
Which is really combination from comments above. Thanks @aaronrenner @dustinfarris |
Given that I find it hard to come by documentation on items such as this, Id like to add an update that may help contemporary Phoenix devs (ca. 2024 / phx 1.7 / ecto_sql 3.10 / ex_machina ~2.7). Ill also mention that I am coming back to Phoenix after not touching Elixir for ~5 years, and likely have a fair amount of naivety. Using anything that returns a changeset produced an error for me:
I was able to accomplish this goal - avoiding duplication of my app's pw hashing code - by extracting the changeset # test/support/factories/admin_factories.ex
defmodule MyAppElixir.Factories.AdminFactory do
use ExMachina.Ecto, repo: MyAppElixir.Repo
def admin_factory do
%Admin{
name: sequence(:name, &"Testy McTesterson #{&1}"),
email: sequence(:email, &"email-#{&1}@example.com"),
hashed_password: "_"
}
end
def set_password(admin, password) do
admin_changeset_with_pw_hash =
admin
|> Admin.registration_changeset(%{"password" => password})
%{hashed_password: hashed_pw} = admin_changeset_with_pw_hash.changes
%{admin | hashed_password: hashed_pw}
end
end And I call this in tests as: build(:admin, %{email: "adminbot@example.com"})
|> set_password("1ValidPassword!")
|> insert() |
I have a User model that hashes the user's password when you call
User.changeset/2
(gist here). How do I create a factory that correctly hashes the password when I do the following?I'm new to elixir, so if there is a better approach for automatically hashing the password, please let me know.
The text was updated successfully, but these errors were encountered: