add callsign to users
This commit is contained in:
parent
73d67da457
commit
38920e9f3d
9 changed files with 567 additions and 10 deletions
|
|
@ -181,6 +181,46 @@ defmodule Aprsme.Accounts do
|
||||||
UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token))
|
UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns an `%Ecto.Changeset{}` for changing the user callsign.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> change_user_callsign(user)
|
||||||
|
%Ecto.Changeset{data: %User{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def change_user_callsign(user, attrs \\ %{}) do
|
||||||
|
User.callsign_changeset(user, attrs, validate_callsign: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Updates the user callsign with password validation.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> update_user_callsign(user, "valid password", %{callsign: "K1ABC"})
|
||||||
|
{:ok, %User{}}
|
||||||
|
|
||||||
|
iex> update_user_callsign(user, "invalid password", %{callsign: "K1ABC"})
|
||||||
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
def update_user_callsign(user, password, attrs) do
|
||||||
|
changeset =
|
||||||
|
user
|
||||||
|
|> User.callsign_changeset(attrs)
|
||||||
|
|> User.validate_current_password(password)
|
||||||
|
|
||||||
|
Ecto.Multi.new()
|
||||||
|
|> Ecto.Multi.update(:user, changeset)
|
||||||
|
|> Repo.transaction()
|
||||||
|
|> case do
|
||||||
|
{:ok, %{user: user}} -> {:ok, user}
|
||||||
|
{:error, :user, changeset, _} -> {:error, changeset}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns an `%Ecto.Changeset{}` for changing the user password.
|
Returns an `%Ecto.Changeset{}` for changing the user password.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ defmodule Aprsme.Accounts.User do
|
||||||
|
|
||||||
schema "users" do
|
schema "users" do
|
||||||
field(:email, :string)
|
field(:email, :string)
|
||||||
|
field(:callsign, :string)
|
||||||
field(:password, :string, virtual: true, redact: true)
|
field(:password, :string, virtual: true, redact: true)
|
||||||
field(:hashed_password, :string, redact: true)
|
field(:hashed_password, :string, redact: true)
|
||||||
field(:confirmed_at, :naive_datetime)
|
field(:confirmed_at, :naive_datetime)
|
||||||
|
|
@ -38,8 +39,9 @@ defmodule Aprsme.Accounts.User do
|
||||||
"""
|
"""
|
||||||
def registration_changeset(user, attrs, opts \\ []) do
|
def registration_changeset(user, attrs, opts \\ []) do
|
||||||
user
|
user
|
||||||
|> cast(attrs, [:email, :password])
|
|> cast(attrs, [:email, :password, :callsign])
|
||||||
|> validate_email(opts)
|
|> validate_email(opts)
|
||||||
|
|> validate_callsign(opts)
|
||||||
|> validate_password(opts)
|
|> validate_password(opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -51,6 +53,17 @@ defmodule Aprsme.Accounts.User do
|
||||||
|> maybe_validate_unique_email(opts)
|
|> maybe_validate_unique_email(opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp validate_callsign(changeset, opts) do
|
||||||
|
changeset
|
||||||
|
|> validate_required([:callsign])
|
||||||
|
|> validate_format(:callsign, ~r/^[A-Z]{1,2}[0-9]{1,2}[A-Z]{1,3}$/i,
|
||||||
|
message: "must be a valid amateur radio callsign"
|
||||||
|
)
|
||||||
|
|> update_change(:callsign, &String.upcase/1)
|
||||||
|
|> validate_length(:callsign, min: 3, max: 10)
|
||||||
|
|> maybe_validate_unique_callsign(opts)
|
||||||
|
end
|
||||||
|
|
||||||
defp validate_password(changeset, opts) do
|
defp validate_password(changeset, opts) do
|
||||||
changeset
|
changeset
|
||||||
|> validate_required([:password])
|
|> validate_required([:password])
|
||||||
|
|
@ -95,6 +108,19 @@ defmodule Aprsme.Accounts.User do
|
||||||
|
|
||||||
defp do_validate_unique_email(changeset, false), do: changeset
|
defp do_validate_unique_email(changeset, false), do: changeset
|
||||||
|
|
||||||
|
defp maybe_validate_unique_callsign(changeset, opts) do
|
||||||
|
validate_callsign? = Keyword.get(opts, :validate_callsign, true)
|
||||||
|
do_validate_unique_callsign(changeset, validate_callsign?)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_validate_unique_callsign(changeset, true) do
|
||||||
|
changeset
|
||||||
|
|> unsafe_validate_unique(:callsign, Aprsme.Repo)
|
||||||
|
|> unique_constraint(:callsign)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_validate_unique_callsign(changeset, false), do: changeset
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
A user changeset for changing the email.
|
A user changeset for changing the email.
|
||||||
|
|
||||||
|
|
@ -110,6 +136,21 @@ defmodule Aprsme.Accounts.User do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
A user changeset for changing the callsign.
|
||||||
|
|
||||||
|
It requires the callsign to change otherwise an error is added.
|
||||||
|
"""
|
||||||
|
def callsign_changeset(user, attrs, opts \\ []) do
|
||||||
|
user
|
||||||
|
|> cast(attrs, [:callsign])
|
||||||
|
|> validate_callsign(opts)
|
||||||
|
|> case do
|
||||||
|
%{changes: %{callsign: _}} = changeset -> changeset
|
||||||
|
%{} = changeset -> add_error(changeset, :callsign, "did not change")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
A user changeset for changing the password.
|
A user changeset for changing the password.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,28 @@ defmodule AprsmeWeb.UserRegistrationLive do
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Callsign</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name={Phoenix.HTML.Form.input_name(f, :callsign)}
|
||||||
|
value={Phoenix.HTML.Form.input_value(f, :callsign) || ""}
|
||||||
|
class={[
|
||||||
|
"input input-bordered w-full bg-base-100 text-base-content",
|
||||||
|
Keyword.has_key?(@changeset.errors, :callsign) && "input-error"
|
||||||
|
]}
|
||||||
|
placeholder="Enter your amateur radio callsign"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label :if={Keyword.has_key?(@changeset.errors, :callsign)} class="label">
|
||||||
|
<span class="label-text-alt text-error">
|
||||||
|
{translate_error(Keyword.get(@changeset.errors, :callsign))}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-control w-full">
|
<div class="form-control w-full">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Password</span>
|
<span class="label-text">Password</span>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ defmodule AprsmeWeb.UserSettingsLive do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use AprsmeWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
|
import AprsmeWeb.CoreComponents, only: [translate_error: 1, simple_form: 1]
|
||||||
|
|
||||||
alias Aprsme.Accounts
|
alias Aprsme.Accounts
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
|
|
@ -10,10 +12,10 @@ defmodule AprsmeWeb.UserSettingsLive do
|
||||||
<div class="hero-content flex-col w-full max-w-4xl">
|
<div class="hero-content flex-col w-full max-w-4xl">
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8">
|
||||||
<h1 class="text-4xl font-bold">Account Settings</h1>
|
<h1 class="text-4xl font-bold">Account Settings</h1>
|
||||||
<p class="text-base-content/70">Update your email address and password</p>
|
<p class="text-base-content/70">Update your email address, callsign, and password</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 w-full">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 w-full">
|
||||||
<!-- Change Email Section -->
|
<!-- Change Email Section -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<div class="card bg-base-100 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
@ -38,10 +40,18 @@ defmodule AprsmeWeb.UserSettingsLive do
|
||||||
type="email"
|
type="email"
|
||||||
name={Phoenix.HTML.Form.input_name(f, :email)}
|
name={Phoenix.HTML.Form.input_name(f, :email)}
|
||||||
value={Phoenix.HTML.Form.input_value(f, :email) || ""}
|
value={Phoenix.HTML.Form.input_value(f, :email) || ""}
|
||||||
class="input input-bordered w-full bg-base-100 text-base-content"
|
class={[
|
||||||
|
"input input-bordered w-full bg-base-100 text-base-content",
|
||||||
|
Keyword.has_key?(@email_changeset.errors, :email) && "input-error"
|
||||||
|
]}
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<label :if={Keyword.has_key?(@email_changeset.errors, :email)} class="label">
|
||||||
|
<span class="label-text-alt text-error">
|
||||||
|
{translate_error(Keyword.get(@email_changeset.errors, :email))}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control w-full">
|
<div class="form-control w-full">
|
||||||
|
|
@ -53,10 +63,21 @@ defmodule AprsmeWeb.UserSettingsLive do
|
||||||
name="current_password"
|
name="current_password"
|
||||||
id="current_password_for_email"
|
id="current_password_for_email"
|
||||||
value={@email_form_current_password || ""}
|
value={@email_form_current_password || ""}
|
||||||
class="input input-bordered w-full bg-base-100 text-base-content"
|
class={[
|
||||||
|
"input input-bordered w-full bg-base-100 text-base-content",
|
||||||
|
Keyword.has_key?(@email_changeset.errors, :current_password) && "input-error"
|
||||||
|
]}
|
||||||
placeholder="Enter current password"
|
placeholder="Enter current password"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<label
|
||||||
|
:if={Keyword.has_key?(@email_changeset.errors, :current_password)}
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
<span class="label-text-alt text-error">
|
||||||
|
{translate_error(Keyword.get(@email_changeset.errors, :current_password))}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control mt-6">
|
<div class="form-control mt-6">
|
||||||
|
|
@ -68,6 +89,79 @@ defmodule AprsmeWeb.UserSettingsLive do
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Change Callsign Section -->
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Change Callsign</h2>
|
||||||
|
|
||||||
|
<.simple_form
|
||||||
|
:let={f}
|
||||||
|
id="callsign_form"
|
||||||
|
for={@callsign_changeset}
|
||||||
|
phx-submit="update_callsign"
|
||||||
|
phx-change="validate_callsign"
|
||||||
|
>
|
||||||
|
<div :if={@callsign_changeset.action == :insert} class="alert alert-error mb-4">
|
||||||
|
<span>Oops, something went wrong! Please check the errors below.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Callsign</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name={Phoenix.HTML.Form.input_name(f, :callsign)}
|
||||||
|
value={Phoenix.HTML.Form.input_value(f, :callsign) || ""}
|
||||||
|
class={[
|
||||||
|
"input input-bordered w-full bg-base-100 text-base-content",
|
||||||
|
Keyword.has_key?(@callsign_changeset.errors, :callsign) && "input-error"
|
||||||
|
]}
|
||||||
|
placeholder="Enter your callsign"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label :if={Keyword.has_key?(@callsign_changeset.errors, :callsign)} class="label">
|
||||||
|
<span class="label-text-alt text-error">
|
||||||
|
{translate_error(Keyword.get(@callsign_changeset.errors, :callsign))}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Current password</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="current_password"
|
||||||
|
id="current_password_for_callsign"
|
||||||
|
value={@callsign_form_current_password || ""}
|
||||||
|
class={[
|
||||||
|
"input input-bordered w-full bg-base-100 text-base-content",
|
||||||
|
Keyword.has_key?(@callsign_changeset.errors, :current_password) && "input-error"
|
||||||
|
]}
|
||||||
|
placeholder="Enter current password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
:if={Keyword.has_key?(@callsign_changeset.errors, :current_password)}
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
<span class="label-text-alt text-error">
|
||||||
|
{translate_error(Keyword.get(@callsign_changeset.errors, :current_password))}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mt-6">
|
||||||
|
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Changing...">
|
||||||
|
Change Callsign
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</.simple_form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Change Password Section -->
|
<!-- Change Password Section -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<div class="card bg-base-100 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
@ -101,10 +195,18 @@ defmodule AprsmeWeb.UserSettingsLive do
|
||||||
type="password"
|
type="password"
|
||||||
name={Phoenix.HTML.Form.input_name(f, :password)}
|
name={Phoenix.HTML.Form.input_name(f, :password)}
|
||||||
value={Phoenix.HTML.Form.input_value(f, :password) || ""}
|
value={Phoenix.HTML.Form.input_value(f, :password) || ""}
|
||||||
class="input input-bordered w-full bg-base-100 text-base-content"
|
class={[
|
||||||
|
"input input-bordered w-full bg-base-100 text-base-content",
|
||||||
|
Keyword.has_key?(@password_changeset.errors, :password) && "input-error"
|
||||||
|
]}
|
||||||
placeholder="Enter new password"
|
placeholder="Enter new password"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<label :if={Keyword.has_key?(@password_changeset.errors, :password)} class="label">
|
||||||
|
<span class="label-text-alt text-error">
|
||||||
|
{translate_error(Keyword.get(@password_changeset.errors, :password))}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control w-full">
|
<div class="form-control w-full">
|
||||||
|
|
@ -115,10 +217,24 @@ defmodule AprsmeWeb.UserSettingsLive do
|
||||||
type="password"
|
type="password"
|
||||||
name={Phoenix.HTML.Form.input_name(f, :password_confirmation)}
|
name={Phoenix.HTML.Form.input_name(f, :password_confirmation)}
|
||||||
value={Phoenix.HTML.Form.input_value(f, :password_confirmation) || ""}
|
value={Phoenix.HTML.Form.input_value(f, :password_confirmation) || ""}
|
||||||
class="input input-bordered w-full bg-base-100 text-base-content"
|
class={[
|
||||||
|
"input input-bordered w-full bg-base-100 text-base-content",
|
||||||
|
Keyword.has_key?(@password_changeset.errors, :password_confirmation) &&
|
||||||
|
"input-error"
|
||||||
|
]}
|
||||||
placeholder="Confirm new password"
|
placeholder="Confirm new password"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<label
|
||||||
|
:if={Keyword.has_key?(@password_changeset.errors, :password_confirmation)}
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
<span class="label-text-alt text-error">
|
||||||
|
{translate_error(
|
||||||
|
Keyword.get(@password_changeset.errors, :password_confirmation)
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control w-full">
|
<div class="form-control w-full">
|
||||||
|
|
@ -130,10 +246,21 @@ defmodule AprsmeWeb.UserSettingsLive do
|
||||||
name="current_password"
|
name="current_password"
|
||||||
id="current_password_for_password"
|
id="current_password_for_password"
|
||||||
value={@current_password || ""}
|
value={@current_password || ""}
|
||||||
class="input input-bordered w-full bg-base-100 text-base-content"
|
class={[
|
||||||
|
"input input-bordered w-full bg-base-100 text-base-content",
|
||||||
|
Keyword.has_key?(@password_changeset.errors, :current_password) && "input-error"
|
||||||
|
]}
|
||||||
placeholder="Enter current password"
|
placeholder="Enter current password"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<label
|
||||||
|
:if={Keyword.has_key?(@password_changeset.errors, :current_password)}
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
<span class="label-text-alt text-error">
|
||||||
|
{translate_error(Keyword.get(@password_changeset.errors, :current_password))}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control mt-6">
|
<div class="form-control mt-6">
|
||||||
|
|
@ -170,8 +297,11 @@ defmodule AprsmeWeb.UserSettingsLive do
|
||||||
socket
|
socket
|
||||||
|> assign(:current_password, nil)
|
|> assign(:current_password, nil)
|
||||||
|> assign(:email_form_current_password, nil)
|
|> assign(:email_form_current_password, nil)
|
||||||
|
|> assign(:callsign_form_current_password, nil)
|
||||||
|> assign(:current_email, user.email)
|
|> assign(:current_email, user.email)
|
||||||
|
|> assign(:current_callsign, user.callsign)
|
||||||
|> assign(:email_changeset, Accounts.change_user_email(user))
|
|> assign(:email_changeset, Accounts.change_user_email(user))
|
||||||
|
|> assign(:callsign_changeset, Accounts.change_user_callsign(user))
|
||||||
|> assign(:password_changeset, Accounts.change_user_password(user))
|
|> assign(:password_changeset, Accounts.change_user_password(user))
|
||||||
|> assign(:trigger_submit, false)
|
|> assign(:trigger_submit, false)
|
||||||
|
|
||||||
|
|
@ -211,6 +341,33 @@ defmodule AprsmeWeb.UserSettingsLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("validate_callsign", params, socket) do
|
||||||
|
%{"current_password" => password, "user" => user_params} = params
|
||||||
|
callsign_changeset = Accounts.change_user_callsign(socket.assigns.current_user, user_params)
|
||||||
|
|
||||||
|
socket =
|
||||||
|
assign(socket,
|
||||||
|
callsign_changeset: Map.put(callsign_changeset, :action, :validate),
|
||||||
|
callsign_form_current_password: password
|
||||||
|
)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("update_callsign", params, socket) do
|
||||||
|
%{"current_password" => password, "user" => user_params} = params
|
||||||
|
user = socket.assigns.current_user
|
||||||
|
|
||||||
|
case Accounts.update_user_callsign(user, password, user_params) do
|
||||||
|
{:ok, _user} ->
|
||||||
|
info = "Callsign updated successfully."
|
||||||
|
{:noreply, socket |> put_flash(:info, info) |> push_navigate(to: ~p"/users/settings")}
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
{:noreply, assign(socket, :callsign_changeset, Map.put(changeset, :action, :insert))}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("validate_password", params, socket) do
|
def handle_event("validate_password", params, socket) do
|
||||||
%{"current_password" => password, "user" => user_params} = params
|
%{"current_password" => password, "user" => user_params} = params
|
||||||
password_changeset = Accounts.change_user_password(socket.assigns.current_user, user_params)
|
password_changeset = Accounts.change_user_password(socket.assigns.current_user, user_params)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
defmodule Aprsme.Repo.Migrations.AddCallsignToUsers do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:users) do
|
||||||
|
add :callsign, :string, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
create unique_index(:users, [:callsign])
|
||||||
|
end
|
||||||
|
end
|
||||||
96
test/aprsme/accounts/user_test.exs
Normal file
96
test/aprsme/accounts/user_test.exs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
defmodule Aprsme.Accounts.UserTest do
|
||||||
|
use Aprsme.DataCase, async: true
|
||||||
|
|
||||||
|
alias Aprsme.Accounts.User
|
||||||
|
|
||||||
|
describe "registration_changeset/2" do
|
||||||
|
test "requires callsign" do
|
||||||
|
changeset = User.registration_changeset(%User{}, %{})
|
||||||
|
errors = errors_on(changeset)
|
||||||
|
assert Map.has_key?(errors, :callsign)
|
||||||
|
assert "can't be blank" in errors.callsign
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates callsign format" do
|
||||||
|
invalid_callsigns = ["123", "TOOLONGCALLSIGN", "NO SPACES", "nodigit"]
|
||||||
|
|
||||||
|
for callsign <- invalid_callsigns do
|
||||||
|
changeset =
|
||||||
|
User.registration_changeset(%User{}, %{
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "valid_password123",
|
||||||
|
callsign: callsign
|
||||||
|
})
|
||||||
|
|
||||||
|
errors = errors_on(changeset)
|
||||||
|
assert Map.has_key?(errors, :callsign), "Expected callsign error for: #{callsign}"
|
||||||
|
assert "must be a valid amateur radio callsign" in errors.callsign
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates empty callsign" do
|
||||||
|
changeset =
|
||||||
|
User.registration_changeset(%User{}, %{
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "valid_password123",
|
||||||
|
callsign: ""
|
||||||
|
})
|
||||||
|
|
||||||
|
errors = errors_on(changeset)
|
||||||
|
assert Map.has_key?(errors, :callsign)
|
||||||
|
assert "can't be blank" in errors.callsign
|
||||||
|
end
|
||||||
|
|
||||||
|
test "accepts valid callsigns" do
|
||||||
|
valid_callsigns = ["K1ABC", "W2XYZ", "AA3BB", "KG4AAA", "N7PQR", "VE3ABC"]
|
||||||
|
|
||||||
|
for callsign <- valid_callsigns do
|
||||||
|
changeset =
|
||||||
|
User.registration_changeset(%User{}, %{
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "valid_password123",
|
||||||
|
callsign: callsign
|
||||||
|
})
|
||||||
|
|
||||||
|
assert changeset.valid?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "converts callsign to uppercase" do
|
||||||
|
changeset =
|
||||||
|
User.registration_changeset(%User{}, %{
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "valid_password123",
|
||||||
|
callsign: "k1abc"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert get_change(changeset, :callsign) == "K1ABC"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "callsign_changeset/2" do
|
||||||
|
test "requires callsign to change" do
|
||||||
|
user = %User{callsign: "K1ABC"}
|
||||||
|
changeset = User.callsign_changeset(user, %{callsign: "K1ABC"})
|
||||||
|
errors = errors_on(changeset)
|
||||||
|
assert Map.has_key?(errors, :callsign)
|
||||||
|
assert "did not change" in errors.callsign
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates new callsign format" do
|
||||||
|
user = %User{callsign: "K1ABC"}
|
||||||
|
|
||||||
|
changeset = User.callsign_changeset(user, %{callsign: "INVALID"})
|
||||||
|
errors = errors_on(changeset)
|
||||||
|
assert Map.has_key?(errors, :callsign)
|
||||||
|
assert "must be a valid amateur radio callsign" in errors.callsign
|
||||||
|
end
|
||||||
|
|
||||||
|
test "accepts valid callsign change" do
|
||||||
|
user = %User{callsign: "K1ABC"}
|
||||||
|
changeset = User.callsign_changeset(user, %{callsign: "W2XYZ"})
|
||||||
|
assert changeset.valid?
|
||||||
|
assert get_change(changeset, :callsign) == "W2XYZ"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -28,11 +28,12 @@ defmodule AprsmeWeb.UserRegistrationLiveTest do
|
||||||
result =
|
result =
|
||||||
lv
|
lv
|
||||||
|> element("#registration_form")
|
|> element("#registration_form")
|
||||||
|> render_change(user: %{"email" => "with spaces", "password" => "too short"})
|
|> render_change(user: %{"email" => "with spaces", "password" => "too short", "callsign" => "INVALID"})
|
||||||
|
|
||||||
assert result =~ "Register"
|
assert result =~ "Register"
|
||||||
assert result =~ "must have the @ sign and no spaces"
|
assert result =~ "must have the @ sign and no spaces"
|
||||||
assert result =~ "should be at least 12 character"
|
assert result =~ "should be at least 12 character"
|
||||||
|
assert result =~ "must be a valid amateur radio callsign"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
183
test/aprsme_web/live/user_settings_live_test.exs
Normal file
183
test/aprsme_web/live/user_settings_live_test.exs
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
defmodule AprsmeWeb.UserSettingsLiveTest do
|
||||||
|
use AprsmeWeb.ConnCase
|
||||||
|
|
||||||
|
import Aprsme.AccountsFixtures
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
|
alias Aprsme.Accounts
|
||||||
|
|
||||||
|
describe "Settings page" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
user = user_fixture()
|
||||||
|
conn = log_in_user(conn, user)
|
||||||
|
{:ok, conn: conn, user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders settings page", %{conn: conn} do
|
||||||
|
{:ok, _lv, html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
assert html =~ "Change Email"
|
||||||
|
assert html =~ "Change Callsign"
|
||||||
|
assert html =~ "Change Password"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects if user is not logged in" do
|
||||||
|
conn = build_conn()
|
||||||
|
|
||||||
|
{:error, redirect} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
assert {:redirect, %{to: path, flash: flash}} = redirect
|
||||||
|
assert path == ~p"/users/log_in"
|
||||||
|
assert %{"error" => "You must log in to access this page."} = flash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update callsign form" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
user = user_fixture()
|
||||||
|
conn = log_in_user(conn, user)
|
||||||
|
{:ok, conn: conn, user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates the user callsign", %{conn: conn, user: user} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
lv
|
||||||
|
|> form("#callsign_form", %{
|
||||||
|
"current_password" => valid_user_password(),
|
||||||
|
"user" => %{"callsign" => "W9NEW"}
|
||||||
|
})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
# The form should redirect after success
|
||||||
|
flash = assert_redirect(lv, ~p"/users/settings")
|
||||||
|
assert flash["info"] == "Callsign updated successfully."
|
||||||
|
|
||||||
|
# Verify the callsign was updated
|
||||||
|
updated_user = Accounts.get_user!(user.id)
|
||||||
|
assert updated_user.callsign == "W9NEW"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors with invalid callsign", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> form("#callsign_form", %{
|
||||||
|
"current_password" => valid_user_password(),
|
||||||
|
"user" => %{"callsign" => "INVALID"}
|
||||||
|
})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert result =~ "must be a valid amateur radio callsign"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors with invalid password", %{conn: conn, user: user} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> form("#callsign_form", %{
|
||||||
|
"current_password" => "invalid",
|
||||||
|
"user" => %{"callsign" => "W9NEW"}
|
||||||
|
})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert result =~ "is not valid"
|
||||||
|
|
||||||
|
# Verify the callsign was not updated
|
||||||
|
updated_user = Accounts.get_user!(user.id)
|
||||||
|
assert updated_user.callsign == user.callsign
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update email form" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
user = user_fixture()
|
||||||
|
conn = log_in_user(conn, user)
|
||||||
|
{:ok, conn: conn, user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates the user email", %{conn: conn, user: user} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> form("#email_form", %{
|
||||||
|
"current_password" => valid_user_password(),
|
||||||
|
"user" => %{"email" => unique_user_email()}
|
||||||
|
})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert result =~ "A link to confirm your email"
|
||||||
|
assert Accounts.get_user_by_email(user.email)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors with invalid data (phx-change)", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> element("#email_form")
|
||||||
|
|> render_change(%{
|
||||||
|
"current_password" => valid_user_password(),
|
||||||
|
"user" => %{"email" => "with spaces"}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert result =~ "must have the @ sign and no spaces"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update password form" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
user = user_fixture()
|
||||||
|
conn = log_in_user(conn, user)
|
||||||
|
{:ok, conn: conn, user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates the user password", %{conn: conn, user: user} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
form =
|
||||||
|
form(lv, "#password_form", %{
|
||||||
|
"current_password" => valid_user_password(),
|
||||||
|
"user" => %{
|
||||||
|
"email" => user.email,
|
||||||
|
"password" => "new valid password",
|
||||||
|
"password_confirmation" => "new valid password"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
render_submit(form)
|
||||||
|
|
||||||
|
new_password_conn = follow_trigger_action(form, conn)
|
||||||
|
|
||||||
|
assert redirected_to(new_password_conn) == ~p"/users/settings"
|
||||||
|
|
||||||
|
assert get_session(new_password_conn, :user_token) != get_session(conn, :user_token)
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(new_password_conn.assigns.flash, :info) =~
|
||||||
|
"Password updated successfully"
|
||||||
|
|
||||||
|
assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors with invalid data (phx-change)", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
||||||
|
|
||||||
|
result =
|
||||||
|
lv
|
||||||
|
|> element("#password_form")
|
||||||
|
|> render_change(%{
|
||||||
|
"current_password" => valid_user_password(),
|
||||||
|
"user" => %{
|
||||||
|
"password" => "short",
|
||||||
|
"password_confirmation" => "does not match"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert result =~ "should be at least 12 character"
|
||||||
|
assert result =~ "does not match password"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -7,10 +7,16 @@ defmodule Aprsme.AccountsFixtures do
|
||||||
def unique_user_email, do: "user#{System.unique_integer()}@example.com"
|
def unique_user_email, do: "user#{System.unique_integer()}@example.com"
|
||||||
def valid_user_password, do: "hello world!"
|
def valid_user_password, do: "hello world!"
|
||||||
|
|
||||||
|
def unique_user_callsign do
|
||||||
|
num = rem(System.unique_integer([:positive]), 99) + 1
|
||||||
|
"K#{num}ABC"
|
||||||
|
end
|
||||||
|
|
||||||
def valid_user_attributes(attrs \\ %{}) do
|
def valid_user_attributes(attrs \\ %{}) do
|
||||||
Enum.into(attrs, %{
|
Enum.into(attrs, %{
|
||||||
email: unique_user_email(),
|
email: unique_user_email(),
|
||||||
password: valid_user_password()
|
password: valid_user_password(),
|
||||||
|
callsign: unique_user_callsign()
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue