diff --git a/lib/basenji/accounts.ex b/lib/basenji/accounts.ex
index 7df93d3..235abf7 100644
--- a/lib/basenji/accounts.ex
+++ b/lib/basenji/accounts.ex
@@ -6,6 +6,7 @@ defmodule Basenji.Accounts do
import Basenji.ContextUtils
import Ecto.Query, warn: false
+ alias Basenji.Accounts.APIToken
alias Basenji.Accounts.User
alias Basenji.Accounts.UserNotifier
alias Basenji.Accounts.UserToken
@@ -13,7 +14,7 @@ defmodule Basenji.Accounts do
def register_user(attrs) do
%User{}
- |> User.email_changeset(attrs)
+ |> User.changeset(attrs)
|> Repo.insert()
end
@@ -145,6 +146,37 @@ defmodule Basenji.Accounts do
:ok
end
+ def create_api_token(%User{} = user) do
+ {token, user_token} = APIToken.build_api_token(user)
+ Repo.insert!(user_token)
+ token
+ end
+
+ def verify_api_token(token) do
+ case Base.url_decode64(token, padding: false) do
+ {:ok, decoded_token} ->
+ hashed_token = :crypto.hash(APIToken.hash_algorithm(), decoded_token)
+
+ from(t in APIToken,
+ where: t.token == ^hashed_token and t.inserted_at > ago(365, "day"),
+ join: user in assoc(t, :user),
+ select: user
+ )
+ |> Repo.one()
+
+ :error ->
+ :error
+ end
+ end
+
+ def delete_api_token(token) do
+ from(t in APIToken,
+ where: t.token == ^token
+ )
+ |> Repo.all()
+ |> Enum.each(&Repo.delete(&1))
+ end
+
defp update_user_and_delete_all_tokens(changeset) do
Repo.transact(fn ->
with {:ok, user} <- Repo.update(changeset) do
diff --git a/lib/basenji/accounts/api_token.ex b/lib/basenji/accounts/api_token.ex
new file mode 100644
index 0000000..723cbd8
--- /dev/null
+++ b/lib/basenji/accounts/api_token.ex
@@ -0,0 +1,49 @@
+defmodule Basenji.Accounts.APIToken do
+ @moduledoc false
+ use Ecto.Schema
+
+ import Ecto.Changeset
+
+ alias Basenji.Accounts.APIToken
+ alias Basenji.Accounts.User
+
+ @attrs [:token, :user_id]
+
+ @hash_algorithm :sha256
+ @rand_size 32
+
+ schema "api_tokens" do
+ field :token, :binary
+ belongs_to :user, User
+
+ timestamps(type: :utc_datetime)
+ end
+
+ def changeset(api_token, attrs) do
+ api_token
+ |> cast(attrs, @attrs)
+ |> validate_changeset()
+ end
+
+ def build_api_token(user) do
+ build_hashed_token(user)
+ end
+
+ defp validate_changeset(changeset) do
+ changeset
+ |> validate_required([:token, :user_id])
+ end
+
+ defp build_hashed_token(user) do
+ token = :crypto.strong_rand_bytes(@rand_size)
+ hashed_token = :crypto.hash(@hash_algorithm, token)
+
+ {Base.url_encode64(token, padding: false),
+ %APIToken{
+ token: hashed_token,
+ user_id: user.id
+ }}
+ end
+
+ def hash_algorithm, do: @hash_algorithm
+end
diff --git a/lib/basenji/accounts/user.ex b/lib/basenji/accounts/user.ex
index de4a17f..421da76 100644
--- a/lib/basenji/accounts/user.ex
+++ b/lib/basenji/accounts/user.ex
@@ -16,6 +16,13 @@ defmodule Basenji.Accounts.User do
timestamps(type: :utc_datetime)
end
+ def changeset(user, attrs \\ %{}, opts \\ []) do
+ user
+ |> cast(attrs, [:email, :password, :confirmed_at, :hashed_password])
+ |> validate_email(opts)
+ |> validate_password(opts)
+ end
+
@doc """
A user changeset for registering or changing the email.
diff --git a/lib/basenji/accounts/user_token.ex b/lib/basenji/accounts/user_token.ex
index d067c8c..b8947b6 100644
--- a/lib/basenji/accounts/user_token.ex
+++ b/lib/basenji/accounts/user_token.ex
@@ -10,8 +10,6 @@ defmodule Basenji.Accounts.UserToken do
@hash_algorithm :sha256
@rand_size 32
- # It is very important to keep the magic link token expiry short,
- # since someone with access to the email may take over the account.
@magic_link_validity_in_minutes 15
@change_email_validity_in_days 7
@session_validity_in_days 14
diff --git a/lib/basenji_web/live/user_live/confirmation.ex b/lib/basenji_web/live/accounts/confirmation_live.ex
similarity index 98%
rename from lib/basenji_web/live/user_live/confirmation.ex
rename to lib/basenji_web/live/accounts/confirmation_live.ex
index 71cffd4..47e6817 100644
--- a/lib/basenji_web/live/user_live/confirmation.ex
+++ b/lib/basenji_web/live/accounts/confirmation_live.ex
@@ -1,4 +1,4 @@
-defmodule BasenjiWeb.UserLive.Confirmation do
+defmodule BasenjiWeb.Accounts.ConfirmationLive do
use BasenjiWeb, :live_view
alias Basenji.Accounts
diff --git a/lib/basenji_web/live/user_live/login.ex b/lib/basenji_web/live/accounts/login_live.ex
similarity index 52%
rename from lib/basenji_web/live/user_live/login.ex
rename to lib/basenji_web/live/accounts/login_live.ex
index ee0260d..40ed78e 100644
--- a/lib/basenji_web/live/user_live/login.ex
+++ b/lib/basenji_web/live/accounts/login_live.ex
@@ -1,4 +1,4 @@
-defmodule BasenjiWeb.UserLive.Login do
+defmodule BasenjiWeb.Accounts.LoginLive do
use BasenjiWeb, :live_view
alias Basenji.Accounts
@@ -12,23 +12,6 @@ defmodule BasenjiWeb.UserLive.Login do
def handle_event("submit_password", _params, socket), do: {:noreply, assign(socket, :trigger_submit, true)}
- def handle_event("submit_magic", %{"user" => %{"email" => email}}, socket) do
- with [user] <- Accounts.list_users(email: email) do
- Accounts.deliver_login_instructions(
- user,
- &url(~p"/users/log-in/#{&1}")
- )
- end
-
- info =
- "If your email is in our system, you will receive instructions for logging in shortly."
-
- socket
- |> put_flash(:info, info)
- |> push_navigate(to: ~p"/users/log-in")
- |> then(&{:noreply, &1})
- end
-
defp assign_form(socket) do
email =
Phoenix.Flash.get(socket.assigns.flash, :email) ||
@@ -38,8 +21,6 @@ defmodule BasenjiWeb.UserLive.Login do
assign(socket, form: form, trigger_submit: false)
end
- defp local_mail_adapter?, do: Application.get_env(:basenji, Basenji.Mailer)[:adapter] == Local
-
def render(assigns) do
~H"""
@@ -59,40 +40,6 @@ defmodule BasenjiWeb.UserLive.Login do
-
-
- <.icon name="hero-information-circle" class="size-6 shrink-0" />
-
-
You are running the local mail adapter.
-
- To see sent emails, visit <.link href="/dev/mailbox" class="underline">the mailbox page.
-
-
-
-
- <.form
- :let={f}
- for={@form}
- id="login_form_magic"
- action={~p"/users/log-in"}
- phx-submit="submit_magic"
- >
- <.input
- readonly={!!@current_scope}
- field={f[:email]}
- type="email"
- label="Email"
- autocomplete="username"
- required
- phx-mounted={JS.focus()}
- />
- <.button class="btn btn-primary w-full">
- Log in with email →
-
-
-
- or
-
<.form
:let={f}
for={@form}
@@ -116,10 +63,7 @@ defmodule BasenjiWeb.UserLive.Login do
autocomplete="current-password"
/>
<.button class="btn btn-primary w-full" name={@form[:remember_me].name} value="true">
- Log in and stay logged in →
-
- <.button class="btn btn-primary btn-soft w-full mt-2">
- Log in only this time
+ Log in
diff --git a/lib/basenji_web/live/user_live/registration.ex b/lib/basenji_web/live/accounts/registration_live.ex
similarity index 81%
rename from lib/basenji_web/live/user_live/registration.ex
rename to lib/basenji_web/live/accounts/registration_live.ex
index ef703ae..680f686 100644
--- a/lib/basenji_web/live/user_live/registration.ex
+++ b/lib/basenji_web/live/accounts/registration_live.ex
@@ -1,4 +1,4 @@
-defmodule BasenjiWeb.UserLive.Registration do
+defmodule BasenjiWeb.Accounts.RegistrationLive do
use BasenjiWeb, :live_view
alias Basenji.Accounts
@@ -12,17 +12,15 @@ defmodule BasenjiWeb.UserLive.Registration do
end
def handle_event("save", %{"user" => user_params}, socket) do
+ user_params = Map.put(user_params, "confirmed_at", DateTime.utc_now())
+
case Accounts.register_user(user_params) do
{:ok, user} ->
- with {:ok, _} <- Accounts.deliver_login_instructions(user, &url(~p"/users/log-in/#{&1}")) do
- socket
- |> put_flash(
- :info,
- "An email was sent to #{user.email}, please access it to confirm your account."
- )
- |> push_navigate(to: ~p"/users/log-in")
- |> then(&{:noreply, &1})
- end
+ IO.inspect(user)
+
+ socket
+ |> push_navigate(to: ~p"/users/log-in")
+ |> then(&{:noreply, &1})
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
@@ -64,6 +62,15 @@ defmodule BasenjiWeb.UserLive.Registration do
phx-mounted={JS.focus()}
/>
+ <.input
+ field={@form[:password]}
+ type="password"
+ label="Password"
+ autocomplete="password"
+ required
+ phx-mounted={JS.focus()}
+ />
+
<.button phx-disable-with="Creating account..." class="btn btn-primary w-full">
Create an account
diff --git a/lib/basenji_web/live/user_live/settings.ex b/lib/basenji_web/live/accounts/settings_live.ex
similarity index 99%
rename from lib/basenji_web/live/user_live/settings.ex
rename to lib/basenji_web/live/accounts/settings_live.ex
index 8dbab16..3de616c 100644
--- a/lib/basenji_web/live/user_live/settings.ex
+++ b/lib/basenji_web/live/accounts/settings_live.ex
@@ -1,4 +1,4 @@
-defmodule BasenjiWeb.UserLive.Settings do
+defmodule BasenjiWeb.Accounts.SettingsLive do
use BasenjiWeb, :live_view
alias Basenji.Accounts
diff --git a/lib/basenji_web/router.ex b/lib/basenji_web/router.ex
index d3a63d8..5aefb76 100644
--- a/lib/basenji_web/router.ex
+++ b/lib/basenji_web/router.ex
@@ -119,8 +119,8 @@ defmodule BasenjiWeb.Router do
live_session :require_authenticated_user,
on_mount: [{BasenjiWeb.UserAuth, :require_authenticated}] do
- live "/users/settings", UserLive.Settings, :edit
- live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
+ live "/users/settings", Accounts.SettingsLive, :edit
+ live "/users/settings/confirm-email/:token", Accounts.SettingsLive, :confirm_email
end
post "/users/update-password", UserSessionController, :update_password
@@ -131,9 +131,9 @@ defmodule BasenjiWeb.Router do
live_session :current_user,
on_mount: [{BasenjiWeb.UserAuth, :mount_current_scope}] do
- live "/users/register", UserLive.Registration, :new
- live "/users/log-in", UserLive.Login, :new
- live "/users/log-in/:token", UserLive.Confirmation, :new
+ live "/users/register", Accounts.RegistrationLive, :new
+ live "/users/log-in", Accounts.LoginLive, :new
+ live "/users/log-in/:token", Accounts.ConfirmationLive, :new
end
post "/users/log-in", UserSessionController, :create
diff --git a/priv/repo/migrations/20250816193833_api_tokens.exs b/priv/repo/migrations/20250816193833_api_tokens.exs
new file mode 100644
index 0000000..1d84536
--- /dev/null
+++ b/priv/repo/migrations/20250816193833_api_tokens.exs
@@ -0,0 +1,12 @@
+defmodule Basenji.Repo.Migrations.ApiTokens do
+ use Ecto.Migration
+
+ def change do
+ create table(:api_tokens) do
+ add :user_id, references(:users, on_delete: :delete_all), null: false
+ add :token, :binary, null: false
+
+ timestamps(type: :utc_datetime)
+ end
+ end
+end
diff --git a/test/basenji/accounts_test.exs b/test/basenji/accounts_test.exs
index 9eb49a1..eaa6e70 100644
--- a/test/basenji/accounts_test.exs
+++ b/test/basenji/accounts_test.exs
@@ -1,18 +1,24 @@
defmodule Basenji.AccountsTest do
use Basenji.DataCase
- import Basenji.AccountsFixtures
-
alias Basenji.Accounts
alias Basenji.Accounts.{User, UserToken}
+ test "CRUD API tokens" do
+ %{id: id} = user = insert(:user)
+ api_token = Accounts.create_api_token(user)
+ assert api_token
+ assert id == Accounts.verify_api_token(api_token).id
+ assert :ok == Accounts.delete_api_token(api_token)
+ end
+
describe "get_user_by_email/1" do
test "does not return the user if the email does not exist" do
assert Enum.empty?(Accounts.list_users(email: "unknown@example.com"))
end
test "returns the user if the email exists" do
- %{id: id} = user = user_fixture()
+ %{id: id} = user = insert(:user)
assert [%User{id: ^id}] = Accounts.list_users(email: user.email)
end
end
@@ -23,21 +29,22 @@ defmodule Basenji.AccountsTest do
end
test "does not return the user if the password is not valid" do
- user = user_fixture() |> set_password()
+ user = insert(:user)
refute Accounts.get_user_by_email_and_password(user.email, "invalid")
end
test "returns the user if the email and password are valid" do
- %{id: id} = user = user_fixture() |> set_password()
+ password = Faker.Internet.slug()
+ %{id: id} = user = insert(:user, password: password)
assert %User{id: ^id} =
- Accounts.get_user_by_email_and_password(user.email, valid_user_password())
+ Accounts.get_user_by_email_and_password(user.email, password)
end
end
describe "get_user!/1" do
test "returns the user with the given id" do
- %{id: id} = user = user_fixture()
+ %{id: id} = user = insert(:user)
assert {:ok, %User{id: ^id}} = Accounts.get_user(user.id)
end
end
@@ -62,7 +69,7 @@ defmodule Basenji.AccountsTest do
end
test "validates email uniqueness" do
- %{email: email} = user_fixture()
+ %{email: email} = insert(:user)
{:error, changeset} = Accounts.register_user(%{email: email})
assert "has already been taken" in errors_on(changeset).email
@@ -72,8 +79,8 @@ defmodule Basenji.AccountsTest do
end
test "registers users without password" do
- email = unique_user_email()
- {:ok, user} = Accounts.register_user(valid_user_attributes(email: email))
+ email = Faker.Internet.email()
+ user = insert(:user, email: email, hashed_password: nil, confirmed_at: nil, password: nil)
assert user.email == email
assert is_nil(user.hashed_password)
assert is_nil(user.confirmed_at)
@@ -109,12 +116,12 @@ defmodule Basenji.AccountsTest do
describe "deliver_user_update_email_instructions/3" do
setup do
- %{user: user_fixture()}
+ %{user: insert(:user)}
end
test "sends token through notification", %{user: user} do
token =
- extract_user_token(fn url ->
+ TestHelper.extract_user_token(fn url ->
Accounts.deliver_user_update_email_instructions(user, "current@example.com", url)
end)
@@ -128,11 +135,11 @@ defmodule Basenji.AccountsTest do
describe "update_user_email/2" do
setup do
- user = unconfirmed_user_fixture()
- email = unique_user_email()
+ user = insert(:user)
+ email = Faker.Internet.email()
token =
- extract_user_token(fn url ->
+ TestHelper.extract_user_token(fn url ->
Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
end)
@@ -198,7 +205,7 @@ defmodule Basenji.AccountsTest do
describe "update_user_password/2" do
setup do
- %{user: user_fixture()}
+ %{user: insert(:user)}
end
test "validates password", %{user: user} do
@@ -230,7 +237,6 @@ defmodule Basenji.AccountsTest do
})
assert expired_tokens == []
- assert is_nil(user.password)
assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
end
@@ -248,7 +254,7 @@ defmodule Basenji.AccountsTest do
describe "generate_user_session_token/1" do
setup do
- %{user: user_fixture()}
+ %{user: insert(:user)}
end
test "generates a token", %{user: user} do
@@ -261,7 +267,7 @@ defmodule Basenji.AccountsTest do
assert_raise Ecto.ConstraintError, fn ->
Repo.insert!(%UserToken{
token: user_token.token,
- user_id: user_fixture().id,
+ user_id: insert(:user).id,
context: "session"
})
end
@@ -278,7 +284,7 @@ defmodule Basenji.AccountsTest do
describe "get_user_by_session_token/1" do
setup do
- user = user_fixture()
+ user = insert(:user)
token = Accounts.generate_user_session_token(user)
%{user: user, token: token}
end
@@ -303,8 +309,8 @@ defmodule Basenji.AccountsTest do
describe "get_user_by_magic_link_token/1" do
setup do
- user = user_fixture()
- {encoded_token, _hashed_token} = generate_user_magic_link_token(user)
+ user = insert(:user)
+ {encoded_token, _hashed_token} = TestHelper.generate_user_magic_link_token(user)
%{user: user, token: encoded_token}
end
@@ -325,9 +331,9 @@ defmodule Basenji.AccountsTest do
describe "login_user_by_magic_link/1" do
test "confirms user and expires tokens" do
- user = unconfirmed_user_fixture()
+ user = insert(:user, confirmed_at: nil, hashed_password: nil, password: nil)
refute user.confirmed_at
- {encoded_token, hashed_token} = generate_user_magic_link_token(user)
+ {encoded_token, hashed_token} = TestHelper.generate_user_magic_link_token(user)
assert {:ok, {user, [%{token: ^hashed_token}]}} =
Accounts.login_user_by_magic_link(encoded_token)
@@ -336,18 +342,19 @@ defmodule Basenji.AccountsTest do
end
test "returns user and (deleted) token for confirmed user" do
- user = user_fixture()
+ %{id: user_id} = user = insert(:user)
assert user.confirmed_at
- {encoded_token, _hashed_token} = generate_user_magic_link_token(user)
- assert {:ok, {^user, []}} = Accounts.login_user_by_magic_link(encoded_token)
+ {encoded_token, _hashed_token} = TestHelper.generate_user_magic_link_token(user)
+ assert {:ok, {retrieved, []}} = Accounts.login_user_by_magic_link(encoded_token)
+ assert retrieved.id == user_id
# one time use only
assert {:error, :not_found} = Accounts.login_user_by_magic_link(encoded_token)
end
test "raises when unconfirmed user has password set" do
- user = unconfirmed_user_fixture()
+ user = insert(:user, confirmed_at: nil)
{1, nil} = Repo.update_all(User, set: [hashed_password: "hashed"])
- {encoded_token, _hashed_token} = generate_user_magic_link_token(user)
+ {encoded_token, _hashed_token} = TestHelper.generate_user_magic_link_token(user)
assert_raise RuntimeError, ~r/magic link log in is not allowed/, fn ->
Accounts.login_user_by_magic_link(encoded_token)
@@ -357,7 +364,7 @@ defmodule Basenji.AccountsTest do
describe "delete_user_session_token/1" do
test "deletes the token" do
- user = user_fixture()
+ user = insert(:user)
token = Accounts.generate_user_session_token(user)
assert Accounts.delete_user_session_token(token) == :ok
refute Accounts.get_user_by_session_token(token)
@@ -366,12 +373,12 @@ defmodule Basenji.AccountsTest do
describe "deliver_login_instructions/2" do
setup do
- %{user: unconfirmed_user_fixture()}
+ %{user: insert(:user)}
end
test "sends token through notification", %{user: user} do
token =
- extract_user_token(fn url ->
+ TestHelper.extract_user_token(fn url ->
Accounts.deliver_login_instructions(user, url)
end)
diff --git a/test/basenji_web/controllers/user_session_controller_test.exs b/test/basenji_web/controllers/user_session_controller_test.exs
index 7b1cbbf..e6d6200 100644
--- a/test/basenji_web/controllers/user_session_controller_test.exs
+++ b/test/basenji_web/controllers/user_session_controller_test.exs
@@ -1,21 +1,20 @@
defmodule BasenjiWeb.UserSessionControllerTest do
use BasenjiWeb.ConnCase, async: true
- import Basenji.AccountsFixtures
-
alias Basenji.Accounts
setup do
- %{unconfirmed_user: unconfirmed_user_fixture(), user: user_fixture()}
+ %{unconfirmed_user: insert(:user, confirmed_at: nil, password: nil), user: insert(:user)}
end
describe "POST /users/log-in - email and password" do
- test "logs the user in", %{conn: conn, user: user} do
- user = set_password(user)
+ test "logs the user in", %{conn: conn, user: _user} do
+ password = Faker.Internet.slug()
+ user = insert(:user, password: password)
conn =
post(conn, ~p"/users/log-in", %{
- "user" => %{"email" => user.email, "password" => valid_user_password()}
+ "user" => %{"email" => user.email, "password" => password}
})
assert get_session(conn, :user_token)
@@ -29,14 +28,15 @@ defmodule BasenjiWeb.UserSessionControllerTest do
assert response =~ ~p"/users/log-out"
end
- test "logs the user in with remember me", %{conn: conn, user: user} do
- user = set_password(user)
+ test "logs the user in with remember me", %{conn: conn, user: _user} do
+ password = Faker.Internet.slug()
+ user = insert(:user, password: password)
conn =
post(conn, ~p"/users/log-in", %{
"user" => %{
"email" => user.email,
- "password" => valid_user_password(),
+ "password" => password,
"remember_me" => "true"
}
})
@@ -45,8 +45,9 @@ defmodule BasenjiWeb.UserSessionControllerTest do
assert redirected_to(conn) == ~p"/"
end
- test "logs the user in with return to", %{conn: conn, user: user} do
- user = set_password(user)
+ test "logs the user in with return to", %{conn: conn, user: _user} do
+ password = Faker.Internet.slug()
+ user = insert(:user, password: password)
conn =
conn
@@ -54,7 +55,7 @@ defmodule BasenjiWeb.UserSessionControllerTest do
|> post(~p"/users/log-in", %{
"user" => %{
"email" => user.email,
- "password" => valid_user_password()
+ "password" => password
}
})
@@ -75,7 +76,7 @@ defmodule BasenjiWeb.UserSessionControllerTest do
describe "POST /users/log-in - magic link" do
test "logs the user in", %{conn: conn, user: user} do
- {token, _hashed_token} = generate_user_magic_link_token(user)
+ {token, _hashed_token} = TestHelper.generate_user_magic_link_token(user)
conn =
post(conn, ~p"/users/log-in", %{
@@ -94,7 +95,7 @@ defmodule BasenjiWeb.UserSessionControllerTest do
end
test "confirms unconfirmed user", %{conn: conn, unconfirmed_user: user} do
- {token, _hashed_token} = generate_user_magic_link_token(user)
+ {token, _hashed_token} = TestHelper.generate_user_magic_link_token(user)
refute user.confirmed_at
conn =
diff --git a/test/basenji_web/live/user_live/confirmation_test.exs b/test/basenji_web/live/accounts/confirmation_live_test.exs
similarity index 89%
rename from test/basenji_web/live/user_live/confirmation_test.exs
rename to test/basenji_web/live/accounts/confirmation_live_test.exs
index 2c7f9c9..e4ff90b 100644
--- a/test/basenji_web/live/user_live/confirmation_test.exs
+++ b/test/basenji_web/live/accounts/confirmation_live_test.exs
@@ -1,19 +1,18 @@
-defmodule BasenjiWeb.UserLive.ConfirmationTest do
+defmodule BasenjiWeb.Accounts.ConfirmationLiveTest do
use BasenjiWeb.ConnCase, async: true
- import Basenji.AccountsFixtures
import Phoenix.LiveViewTest
alias Basenji.Accounts
setup do
- %{unconfirmed_user: unconfirmed_user_fixture(), confirmed_user: user_fixture()}
+ %{unconfirmed_user: insert(:user, confirmed_at: nil, password: nil), confirmed_user: insert(:user)}
end
describe "Confirm user" do
test "renders confirmation page for unconfirmed user", %{conn: conn, unconfirmed_user: user} do
token =
- extract_user_token(fn url ->
+ TestHelper.extract_user_token(fn url ->
Accounts.deliver_login_instructions(user, url)
end)
@@ -23,7 +22,7 @@ defmodule BasenjiWeb.UserLive.ConfirmationTest do
test "renders login page for confirmed user", %{conn: conn, confirmed_user: user} do
token =
- extract_user_token(fn url ->
+ TestHelper.extract_user_token(fn url ->
Accounts.deliver_login_instructions(user, url)
end)
@@ -34,7 +33,7 @@ defmodule BasenjiWeb.UserLive.ConfirmationTest do
test "confirms the given token once", %{conn: conn, unconfirmed_user: user} do
token =
- extract_user_token(fn url ->
+ TestHelper.extract_user_token(fn url ->
Accounts.deliver_login_instructions(user, url)
end)
@@ -69,7 +68,7 @@ defmodule BasenjiWeb.UserLive.ConfirmationTest do
confirmed_user: user
} do
token =
- extract_user_token(fn url ->
+ TestHelper.extract_user_token(fn url ->
Accounts.deliver_login_instructions(user, url)
end)
diff --git a/test/basenji_web/live/user_live/login_test.exs b/test/basenji_web/live/accounts/login_live_test.exs
similarity index 92%
rename from test/basenji_web/live/user_live/login_test.exs
rename to test/basenji_web/live/accounts/login_live_test.exs
index 0b4ec4e..ad353a7 100644
--- a/test/basenji_web/live/user_live/login_test.exs
+++ b/test/basenji_web/live/accounts/login_live_test.exs
@@ -1,7 +1,6 @@
-defmodule BasenjiWeb.UserLive.LoginTest do
+defmodule BasenjiWeb.Accounts.LoginLiveTest do
use BasenjiWeb.ConnCase, async: true
- import Basenji.AccountsFixtures
import Phoenix.LiveViewTest
alias Basenji.Accounts.UserToken
@@ -18,7 +17,7 @@ defmodule BasenjiWeb.UserLive.LoginTest do
describe "user login - magic link" do
test "sends magic link email when user exists", %{conn: conn} do
- user = user_fixture()
+ user = insert(:user)
{:ok, lv, _html} = live(conn, ~p"/users/log-in")
@@ -47,12 +46,13 @@ defmodule BasenjiWeb.UserLive.LoginTest do
describe "user login - password" do
test "redirects if user logs in with valid credentials", %{conn: conn} do
- user = user_fixture() |> set_password()
+ password = Faker.Internet.slug()
+ user = insert(:user, password: password)
{:ok, lv, _html} = live(conn, ~p"/users/log-in")
form =
- form(lv, "#login_form_password", user: %{email: user.email, password: valid_user_password(), remember_me: true})
+ form(lv, "#login_form_password", user: %{email: user.email, password: password, remember_me: true})
conn = submit_form(form, conn)
@@ -91,7 +91,7 @@ defmodule BasenjiWeb.UserLive.LoginTest do
describe "re-authentication (sudo mode)" do
setup %{conn: conn} do
- user = user_fixture()
+ user = insert(:user)
%{user: user, conn: log_in_user(conn, user)}
end
diff --git a/test/basenji_web/live/user_live/registration_test.exs b/test/basenji_web/live/accounts/registration_live_test.exs
similarity index 86%
rename from test/basenji_web/live/user_live/registration_test.exs
rename to test/basenji_web/live/accounts/registration_live_test.exs
index 346e5d6..f2d0c2d 100644
--- a/test/basenji_web/live/user_live/registration_test.exs
+++ b/test/basenji_web/live/accounts/registration_live_test.exs
@@ -1,7 +1,6 @@
-defmodule BasenjiWeb.UserLive.RegistrationTest do
+defmodule BasenjiWeb.Accounts.RegistrationLiveTest do
use BasenjiWeb.ConnCase, async: true
- import Basenji.AccountsFixtures
import Phoenix.LiveViewTest
describe "Registration page" do
@@ -15,7 +14,7 @@ defmodule BasenjiWeb.UserLive.RegistrationTest do
test "redirects if already logged in", %{conn: conn} do
result =
conn
- |> log_in_user(user_fixture())
+ |> log_in_user(insert(:user))
|> live(~p"/users/register")
|> follow_redirect(conn, ~p"/")
@@ -39,8 +38,14 @@ defmodule BasenjiWeb.UserLive.RegistrationTest do
test "creates account but does not log in", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/users/register")
- email = unique_user_email()
- form = form(lv, "#registration_form", user: valid_user_attributes(email: email))
+ email = Faker.Internet.email()
+
+ form =
+ form(lv, "#registration_form",
+ user: %{
+ email: email
+ }
+ )
{:ok, _lv, html} =
render_submit(form)
@@ -53,7 +58,7 @@ defmodule BasenjiWeb.UserLive.RegistrationTest do
test "renders errors for duplicated email", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/users/register")
- user = user_fixture(%{email: "test@email.com"})
+ user = insert(:user)
result =
lv
diff --git a/test/basenji_web/live/user_live/settings_test.exs b/test/basenji_web/live/accounts/settings_live_test.exs
similarity index 92%
rename from test/basenji_web/live/user_live/settings_test.exs
rename to test/basenji_web/live/accounts/settings_live_test.exs
index dc50819..783752a 100644
--- a/test/basenji_web/live/user_live/settings_test.exs
+++ b/test/basenji_web/live/accounts/settings_live_test.exs
@@ -1,7 +1,6 @@
-defmodule BasenjiWeb.UserLive.SettingsTest do
+defmodule BasenjiWeb.Accounts.SettingsLiveTest do
use BasenjiWeb.ConnCase, async: true
- import Basenji.AccountsFixtures
import Phoenix.LiveViewTest
alias Basenji.Accounts
@@ -10,7 +9,7 @@ defmodule BasenjiWeb.UserLive.SettingsTest do
test "renders settings page", %{conn: conn} do
{:ok, _lv, html} =
conn
- |> log_in_user(user_fixture())
+ |> log_in_user(insert(:user))
|> live(~p"/users/settings")
assert html =~ "Change Email"
@@ -28,7 +27,7 @@ defmodule BasenjiWeb.UserLive.SettingsTest do
test "redirects if user is not in sudo mode", %{conn: conn} do
{:ok, conn} =
conn
- |> log_in_user(user_fixture(),
+ |> log_in_user(insert(:user),
token_authenticated_at: DateTime.add(DateTime.utc_now(:second), -11, :minute)
)
|> live(~p"/users/settings")
@@ -40,12 +39,12 @@ defmodule BasenjiWeb.UserLive.SettingsTest do
describe "update email form" do
setup %{conn: conn} do
- user = user_fixture()
+ user = insert(:user)
%{conn: log_in_user(conn, user), user: user}
end
test "updates the user email", %{conn: conn, user: user} do
- new_email = unique_user_email()
+ new_email = Faker.Internet.email()
{:ok, lv, _html} = live(conn, ~p"/users/settings")
@@ -92,12 +91,12 @@ defmodule BasenjiWeb.UserLive.SettingsTest do
describe "update password form" do
setup %{conn: conn} do
- user = user_fixture()
+ user = insert(:user)
%{conn: log_in_user(conn, user), user: user}
end
test "updates the user password", %{conn: conn, user: user} do
- new_password = valid_user_password()
+ new_password = Faker.Internet.slug()
{:ok, lv, _html} = live(conn, ~p"/users/settings")
@@ -163,15 +162,15 @@ defmodule BasenjiWeb.UserLive.SettingsTest do
describe "confirm email" do
setup %{conn: conn} do
- user = user_fixture()
- email = unique_user_email()
+ user = insert(:user)
+ new_email = Faker.Internet.email()
token =
- extract_user_token(fn url ->
- Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
+ TestHelper.extract_user_token(fn url ->
+ Accounts.deliver_user_update_email_instructions(%{user | email: new_email}, user.email, url)
end)
- %{conn: log_in_user(conn, user), token: token, email: email, user: user}
+ %{conn: log_in_user(conn, user), token: token, email: new_email, user: user}
end
test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do
diff --git a/test/basenji_web/user_auth_test.exs b/test/basenji_web/user_auth_test.exs
index 161314a..75ad51a 100644
--- a/test/basenji_web/user_auth_test.exs
+++ b/test/basenji_web/user_auth_test.exs
@@ -1,10 +1,11 @@
defmodule BasenjiWeb.UserAuthTest do
use BasenjiWeb.ConnCase, async: true
- import Basenji.AccountsFixtures
+ import Ecto.Query
alias Basenji.Accounts
alias Basenji.Accounts.Scope
+ alias Basenji.Accounts.UserToken
alias BasenjiWeb.UserAuth
alias Phoenix.LiveView
alias Phoenix.Socket.Broadcast
@@ -18,7 +19,7 @@ defmodule BasenjiWeb.UserAuthTest do
|> Map.replace!(:secret_key_base, BasenjiWeb.Endpoint.config(:secret_key_base))
|> init_test_session(%{})
- %{user: %{user_fixture() | authenticated_at: DateTime.utc_now(:second)}, conn: conn}
+ %{user: %{insert(:user) | authenticated_at: DateTime.utc_now(:second)}, conn: conn}
end
describe "log_in_user/3" do
@@ -49,7 +50,7 @@ defmodule BasenjiWeb.UserAuthTest do
conn: conn,
user: user
} do
- other_user = user_fixture()
+ other_user = insert(:user)
conn =
conn
@@ -388,4 +389,13 @@ defmodule BasenjiWeb.UserAuthTest do
}
end
end
+
+ def offset_user_token(token, amount_to_add, unit) do
+ dt = DateTime.add(DateTime.utc_now(:second), amount_to_add, unit)
+
+ Basenji.Repo.update_all(
+ from(ut in UserToken, where: ut.token == ^token),
+ set: [inserted_at: dt, authenticated_at: dt]
+ )
+ end
end
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
index 5113cb9..99455bf 100644
--- a/test/support/conn_case.ex
+++ b/test/support/conn_case.ex
@@ -17,7 +17,11 @@ defmodule BasenjiWeb.ConnCase do
use ExUnit.CaseTemplate
+ import Ecto.Query
+
alias Basenji.Accounts.Scope
+ alias Basenji.Accounts.User
+ alias Basenji.Accounts.UserToken
using do
quote do
@@ -48,7 +52,8 @@ defmodule BasenjiWeb.ConnCase do
test context.
"""
def register_and_log_in_user(%{conn: conn} = context) do
- user = Basenji.AccountsFixtures.user_fixture()
+ # insert(:user)
+ user = %User{email: Faker.Internet.email()}
scope = Scope.for_user(user)
opts =
@@ -77,6 +82,11 @@ defmodule BasenjiWeb.ConnCase do
defp maybe_set_token_authenticated_at(_token, nil), do: nil
defp maybe_set_token_authenticated_at(token, authenticated_at) do
- Basenji.AccountsFixtures.override_token_authenticated_at(token, authenticated_at)
+ Basenji.Repo.update_all(
+ from(t in UserToken,
+ where: t.token == ^token
+ ),
+ set: [authenticated_at: authenticated_at]
+ )
end
end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index d44ced6..e521e6c 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -1,6 +1,7 @@
defmodule Basenji.Factory do
@moduledoc false
use ExMachina.Ecto, repo: Basenji.Repo
+ use Basenji.Factory.AccountsFactory
use Basenji.Factory.ComicFactory
use Basenji.Factory.CollectionFactory
end
diff --git a/test/support/factory/accounts_factory.ex b/test/support/factory/accounts_factory.ex
new file mode 100644
index 0000000..68849c0
--- /dev/null
+++ b/test/support/factory/accounts_factory.ex
@@ -0,0 +1,21 @@
+defmodule Basenji.Factory.AccountsFactory do
+ @moduledoc false
+
+ defmacro __using__(_opts) do
+ quote do
+ def user_factory(attrs) do
+ password = Map.get(attrs, :password, Faker.Internet.slug())
+ hashed = if password, do: Bcrypt.hash_pwd_salt(password)
+
+ %Basenji.Accounts.User{
+ email: Faker.Internet.email(),
+ password: password,
+ hashed_password: hashed,
+ confirmed_at: Faker.DateTime.backward(1)
+ }
+ |> merge_attributes(attrs)
+ |> evaluate_lazy_attributes()
+ end
+ end
+ end
+end
diff --git a/test/support/fixtures/accounts_fixtures.ex b/test/support/fixtures/accounts_fixtures.ex
deleted file mode 100644
index ad706d6..0000000
--- a/test/support/fixtures/accounts_fixtures.ex
+++ /dev/null
@@ -1,89 +0,0 @@
-defmodule Basenji.AccountsFixtures do
- @moduledoc """
- This module defines test helpers for creating
- entities via the `Basenji.Accounts` context.
- """
-
- import Ecto.Query
-
- alias Basenji.Accounts
- alias Basenji.Accounts.Scope
-
- def unique_user_email, do: "user#{System.unique_integer()}@example.com"
- def valid_user_password, do: "hello world!"
-
- def valid_user_attributes(attrs \\ %{}) do
- Enum.into(attrs, %{
- email: unique_user_email()
- })
- end
-
- def unconfirmed_user_fixture(attrs \\ %{}) do
- {:ok, user} =
- attrs
- |> valid_user_attributes()
- |> Accounts.register_user()
-
- user
- end
-
- def user_fixture(attrs \\ %{}) do
- user = unconfirmed_user_fixture(attrs)
-
- token =
- extract_user_token(fn url ->
- Accounts.deliver_login_instructions(user, url)
- end)
-
- {:ok, {user, _expired_tokens}} =
- Accounts.login_user_by_magic_link(token)
-
- user
- end
-
- def user_scope_fixture do
- user = user_fixture()
- user_scope_fixture(user)
- end
-
- def user_scope_fixture(user) do
- Scope.for_user(user)
- end
-
- def set_password(user) do
- {:ok, {user, _expired_tokens}} =
- Accounts.update_user_password(user, %{password: valid_user_password()})
-
- user
- end
-
- def extract_user_token(fun) do
- {:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
- [_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
- token
- end
-
- def override_token_authenticated_at(token, authenticated_at) when is_binary(token) do
- Basenji.Repo.update_all(
- from(t in Accounts.UserToken,
- where: t.token == ^token
- ),
- set: [authenticated_at: authenticated_at]
- )
- end
-
- def generate_user_magic_link_token(user) do
- {encoded_token, user_token} = Accounts.UserToken.build_email_token(user, "login")
- Basenji.Repo.insert!(user_token)
- {encoded_token, user_token.token}
- end
-
- def offset_user_token(token, amount_to_add, unit) do
- dt = DateTime.add(DateTime.utc_now(:second), amount_to_add, unit)
-
- Basenji.Repo.update_all(
- from(ut in Accounts.UserToken, where: ut.token == ^token),
- set: [inserted_at: dt, authenticated_at: dt]
- )
- end
-end
diff --git a/test/test_helper.exs b/test/test_helper.exs
index 4d34078..9dcee33 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -4,6 +4,20 @@ ExUnit.start(timeout: :infinity)
Ecto.Adapters.SQL.Sandbox.mode(Basenji.Repo, :manual)
defmodule TestHelper do
+ alias Basenji.Accounts.UserToken
+
+ def extract_user_token(fun) do
+ {:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
+ [_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
+ token
+ end
+
+ def generate_user_magic_link_token(user) do
+ {encoded_token, user_token} = UserToken.build_email_token(user, "login")
+ Basenji.Repo.insert!(user_token)
+ {encoded_token, user_token.token}
+ end
+
def get_tmp_dir, do: Path.join(System.tmp_dir!(), "basenji")
def drain_queue(queue, start_opts \\ [], drain_opts \\ [])