mirror of
https://codeberg.org/JasterV/spazio-solazzo.git
synced 2026-04-26 18:20:03 +00:00
refactor: make phone number optional (#9)
This pull request makes phone numbers optional for both user registrations and bookings, and updates validation, database schema, and UI to reflect this change. The main focus is to ensure that users are no longer required to provide a phone number, and that the application gracefully handles cases where a phone number is absent. **Database & Resource Model Updates** * Made the `phone_number` attribute in the `users` table and the `customer_phone` attribute in the `bookings` table nullable, including migration and resource snapshot updates. [[1]](diffhunk://#diff-baa6aed3674c4d6cbbebeafb076662df02dc4c25231dbd9dc9c8f0534ed1a1bfR1-R29) [[2]](diffhunk://#diff-a401f66b2ae5bfb798eb1bc2221bfeeac943e258950c90d59570b0bae05d3664R1-R244) [[3]](diffhunk://#diff-0c1180d6f6abc19b5987c8703bdee9ef67905535202f950e8327c32bd5b89d8aR1-R82) * Updated Ash resource definitions in `user.ex` and `booking.ex` to allow `phone_number` and `customer_phone` to be `nil`. [[1]](diffhunk://#diff-9194b9d80dce091f6dcb56f784217272ae160e35454c4b4ccc8850ad5ee06e38L152-R152) [[2]](diffhunk://#diff-4b1ddd6d86899f2144c69d142883b8719c755e32c03dbda5da2188208a5ad503L55-R55) [[3]](diffhunk://#diff-4b1ddd6d86899f2144c69d142883b8719c755e32c03dbda5da2188208a5ad503L170-R170) **Validation & Parsing Logic** * Renamed and refactored user registration field validation to `ParseRegistrationFields`, allowing phone numbers to be omitted and trimming input values. Empty phone numbers are now treated as absent rather than as errors. [[1]](diffhunk://#diff-8ffdd76e260e3cda6f0816c8e585ae76b993a90d2519c38185a5fe22b4b49e47L1-R1) [[2]](diffhunk://#diff-8ffdd76e260e3cda6f0816c8e585ae76b993a90d2519c38185a5fe22b4b49e47R14-R60) * Updated the authentication callback logic to trim input values and omit the phone number parameter if it is blank. **User Interface Improvements** * Updated registration and booking forms to indicate that phone numbers are optional, removed the required attribute, and improved placeholder text. [[1]](diffhunk://#diff-f356eb84970d8c9ee6ff1992c297b0cae07bade37ff967c1e6e0de6f8b67081cL101-R115) [[2]](diffhunk://#diff-43c0e1f7a869ee5c43a911bc10dc80cbb265a8672340ef0fa7c1d3009c047f02L92-R92) * Updated email templates and confirmation screens to display "N/A" or "-" when phone numbers are missing. [[1]](diffhunk://#diff-48468ef2d1bb2c33b5ffb40457b77532815c7faf1830932661f665bff58b2177R6-R11) [[2]](diffhunk://#diff-3f33187b4021450b481ce53fe13166addea582c627f2cfbc99c75c7ce5c34857L10-R10) [[3]](diffhunk://#diff-43c0e1f7a869ee5c43a911bc10dc80cbb265a8672340ef0fa7c1d3009c047f02L92-R92) **Profile Management** * Improved profile update flow to ensure the form reflects the latest user data after saving changes. * Made the "Full Name" field explicitly required in the profile form UI.
This commit is contained in:
parent
8dafc3e3fd
commit
2cbce8ec39
23 changed files with 699 additions and 137 deletions
|
|
@ -89,7 +89,7 @@
|
|||
# If you don't want TODO comments to cause `mix credo` to fail, just
|
||||
# set this value to 0 (zero).
|
||||
#
|
||||
{Credo.Check.Design.TagTODO, [exit_status: 2]},
|
||||
{Credo.Check.Design.TagTODO, [exit_status: 0]},
|
||||
|
||||
#
|
||||
## Readability Checks
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ defmodule SpazioSolazzo.Accounts.User do
|
|||
change AshAuthentication.Strategy.MagicLink.SignInChange
|
||||
|
||||
# Conditionally validate name and phone_number for new users
|
||||
change SpazioSolazzo.Accounts.User.Changes.ValidateRegistrationFields
|
||||
change SpazioSolazzo.Accounts.User.Changes.ParseRegistrationFields
|
||||
|
||||
change {AshAuthentication.Strategy.RememberMe.MaybeGenerateTokenChange,
|
||||
strategy_name: :remember_me}
|
||||
|
|
@ -149,7 +149,7 @@ defmodule SpazioSolazzo.Accounts.User do
|
|||
end
|
||||
|
||||
attribute :phone_number, :string do
|
||||
allow_nil? false
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule SpazioSolazzo.Accounts.User.Changes.ValidateRegistrationFields do
|
||||
defmodule SpazioSolazzo.Accounts.User.Changes.ParseRegistrationFields do
|
||||
@moduledoc """
|
||||
Conditionally validates that name and phone_number are present for new user registrations.
|
||||
For existing users (upserts), these fields are not required.
|
||||
|
|
@ -11,28 +11,53 @@ defmodule SpazioSolazzo.Accounts.User.Changes.ValidateRegistrationFields do
|
|||
|
||||
case SpazioSolazzo.Accounts.get_user_by_email(email, authorize?: false) do
|
||||
{:ok, %{phone_number: phone, name: name}} ->
|
||||
# User is already registered, we'll just set the same values it had
|
||||
changeset
|
||||
|> Ash.Changeset.force_change_attribute(:name, name)
|
||||
|> Ash.Changeset.force_change_attribute(:phone_number, phone)
|
||||
|
||||
_ ->
|
||||
# User is not yet registered, we'll parse & validate the new values
|
||||
name = Ash.Changeset.get_argument(changeset, :name)
|
||||
phone = Ash.Changeset.get_argument(changeset, :phone_number)
|
||||
|
||||
changeset
|
||||
|> validate_required_for_registration(:name, name)
|
||||
|> validate_required_for_registration(:phone_number, phone)
|
||||
|> parse_name(name)
|
||||
|> parse_phone_number(phone)
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_required_for_registration(changeset, field, value) do
|
||||
if is_nil(value) || value == "" do
|
||||
Ash.Changeset.add_error(
|
||||
changeset,
|
||||
Ash.Error.Changes.Required.exception(field: field, type: :argument)
|
||||
)
|
||||
defp parse_name(changeset, nil) do
|
||||
Ash.Changeset.add_error(
|
||||
changeset,
|
||||
Ash.Error.Changes.Required.exception(field: :name, type: :argument)
|
||||
)
|
||||
end
|
||||
|
||||
defp parse_name(changeset, value) do
|
||||
value = String.trim(value)
|
||||
|
||||
if value == "" do
|
||||
parse_name(changeset, nil)
|
||||
else
|
||||
Ash.Changeset.change_attribute(changeset, field, value)
|
||||
Ash.Changeset.change_attribute(changeset, :name, value)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_phone_number(changeset, nil) do
|
||||
# The phone number is nullable, this is fine
|
||||
Ash.Changeset.change_attribute(changeset, :phone_number, nil)
|
||||
end
|
||||
|
||||
defp parse_phone_number(changeset, value) do
|
||||
value = String.trim(value)
|
||||
|
||||
if value == "" do
|
||||
# Instead of returning an error, we'll consider an empty phone number
|
||||
# as if the user didn't want to set one, which is valid.
|
||||
parse_name(changeset, nil)
|
||||
else
|
||||
Ash.Changeset.change_attribute(changeset, :phone_number, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
argument :date, :date, allow_nil?: false
|
||||
argument :customer_name, :string, allow_nil?: false
|
||||
argument :customer_email, :string, allow_nil?: false
|
||||
argument :customer_phone, :string, allow_nil?: false
|
||||
argument :customer_phone, :string, allow_nil?: true
|
||||
argument :customer_comment, :string, allow_nil?: true
|
||||
|
||||
change manage_relationship(:time_slot_template_id, :time_slot_template,
|
||||
|
|
@ -167,7 +167,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
attribute :customer_email, :string, allow_nil?: false
|
||||
attribute :start_time, :time, allow_nil?: false
|
||||
attribute :end_time, :time, allow_nil?: false
|
||||
attribute :customer_phone, :string, allow_nil?: false
|
||||
attribute :customer_phone, :string, allow_nil?: true
|
||||
attribute :customer_comment, :string, allow_nil?: true
|
||||
|
||||
attribute :state, :atom do
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@
|
|||
<div style="margin-bottom: 20px;">
|
||||
<p><strong>Customer:</strong> {@customer_name}</p>
|
||||
<p><strong>Email:</strong> <a href={"mailto:#{@customer_email}"}>{@customer_email}</a></p>
|
||||
<p><strong>Phone:</strong> <a href={"tel:#{@customer_phone}"}>{@customer_phone}</a></p>
|
||||
|
||||
<%= if @customer_phone && String.trim(@customer_phone) != "" do %>
|
||||
<p><strong>Phone:</strong> <a href={"tel:#{@customer_phone}"}>{@customer_phone}</a></p>
|
||||
<% else %>
|
||||
<p><strong>Phone:</strong> <a href="#">N/A</a></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<.details_list>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<.detail_item label="Date">{@date}</.detail_item>
|
||||
<.detail_item label="Time">{@start_time} - {@end_time}</.detail_item>
|
||||
<.detail_item label="Email">{@customer_email}</.detail_item>
|
||||
<.detail_item label="Phone">{@customer_phone}</.detail_item>
|
||||
<.detail_item label="Phone">{@customer_phone || "N/A"}</.detail_item>
|
||||
<.detail_item label="Note">{@customer_comment || "N/A"}</.detail_item>
|
||||
</.details_list>
|
||||
|
||||
|
|
|
|||
|
|
@ -74,12 +74,17 @@ defmodule SpazioSolazzoWeb.AuthCallbackLive do
|
|||
) do
|
||||
%{token: token} = socket.assigns
|
||||
remember_me = Map.get(args, "remember_me") == "on"
|
||||
name = String.trim(name)
|
||||
phone_number = String.trim(phone_number)
|
||||
|
||||
{:noreply,
|
||||
redirect(socket,
|
||||
to:
|
||||
~p"/auth/magic/sign-in?token=#{token}&name=#{name}&phone_number=#{phone_number}&remember_me=#{remember_me}"
|
||||
)}
|
||||
url =
|
||||
if phone_number == "" do
|
||||
~p"/auth/magic/sign-in?token=#{token}&name=#{name}&remember_me=#{remember_me}"
|
||||
else
|
||||
~p"/auth/magic/sign-in?token=#{token}&name=#{name}&phone_number=#{phone_number}&remember_me=#{remember_me}"
|
||||
end
|
||||
|
||||
{:noreply, redirect(socket, to: url)}
|
||||
end
|
||||
|
||||
defp extract_email_from_token(token) do
|
||||
|
|
|
|||
|
|
@ -98,21 +98,23 @@
|
|||
for="phone_number"
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||
>
|
||||
Phone Number <span class="text-rose-500">*</span>
|
||||
Phone Number (Optional)
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="tel"
|
||||
name="phone_number"
|
||||
id="phone_number"
|
||||
required
|
||||
class="w-full pl-11 pr-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-shadow"
|
||||
placeholder="+39 123 456 7890"
|
||||
placeholder="+39 123456789"
|
||||
/>
|
||||
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
||||
<.icon name="hero-phone" class="size-5 text-slate-400 dark:text-slate-500" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-slate-500 dark:text-slate-400">
|
||||
Your number will only be used to contact you personally about booking issues, never for marketing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label class="flex items-start gap-3 cursor-pointer group p-3 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-900/50 transition-colors">
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ defmodule SpazioSolazzoWeb.AssetBookingLive do
|
|||
{:noreply, assign(socket, show_success_modal: false)}
|
||||
end
|
||||
|
||||
def handle_info({:create_booking, comment}, socket) do
|
||||
def handle_info({:create_booking, booking_data}, socket) do
|
||||
current_user = socket.assigns.current_user
|
||||
|
||||
result =
|
||||
|
|
@ -64,10 +64,10 @@ defmodule SpazioSolazzoWeb.AssetBookingLive do
|
|||
socket.assigns.asset.id,
|
||||
current_user.id,
|
||||
socket.assigns.selected_date,
|
||||
current_user.name,
|
||||
booking_data.customer_name,
|
||||
current_user.email,
|
||||
current_user.phone_number,
|
||||
comment
|
||||
booking_data.customer_phone,
|
||||
booking_data.customer_comment
|
||||
)
|
||||
|
||||
case result do
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
|
|||
|
||||
def update(assigns, socket) do
|
||||
initial_data = %{
|
||||
"customer_name" => assigns.current_user.name,
|
||||
"customer_phone" => assigns.current_user.phone_number || "",
|
||||
"customer_comment" => ""
|
||||
}
|
||||
|
||||
|
|
@ -25,8 +27,13 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
|
|||
end
|
||||
|
||||
def handle_event("submit_booking", params, socket) do
|
||||
comment = params["customer_comment"] || ""
|
||||
send(self(), {:create_booking, comment})
|
||||
booking_data = %{
|
||||
customer_name: params["customer_name"] || "",
|
||||
customer_phone: params["customer_phone"] || "",
|
||||
customer_comment: params["customer_comment"] || ""
|
||||
}
|
||||
|
||||
send(self(), {:create_booking, booking_data})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
|
|
@ -53,16 +60,25 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
|
|||
>
|
||||
<div class="mt-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Name
|
||||
<label
|
||||
for="customer_name"
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||
>
|
||||
Name <span class="text-rose-500">*</span>
|
||||
</label>
|
||||
<div class="flex items-center gap-3 p-4 bg-gradient-to-r from-slate-50 to-sky-50/50 dark:from-slate-900 dark:to-slate-800 rounded-xl border border-slate-200 dark:border-slate-700">
|
||||
<div class="flex-shrink-0">
|
||||
<.icon name="hero-user" class="size-5 text-sky-600 dark:text-sky-400" />
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
name="customer_name"
|
||||
id="customer_name"
|
||||
value={@form[:customer_name].value}
|
||||
required
|
||||
class="w-full pl-11 pr-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-shadow"
|
||||
placeholder="Your full name"
|
||||
/>
|
||||
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
||||
<.icon name="hero-user" class="size-5 text-slate-400 dark:text-slate-500" />
|
||||
</div>
|
||||
<span class="text-sm font-medium text-slate-700 dark:text-slate-300 truncate">
|
||||
{@current_user.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -81,26 +97,56 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Phone
|
||||
<label
|
||||
for="customer_phone"
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||
>
|
||||
Phone (Optional)
|
||||
</label>
|
||||
<div class="flex items-center gap-3 p-4 bg-gradient-to-r from-slate-50 to-sky-50/50 dark:from-slate-900 dark:to-slate-800 rounded-xl border border-slate-200 dark:border-slate-700">
|
||||
<div class="flex-shrink-0">
|
||||
<.icon name="hero-phone" class="size-5 text-sky-600 dark:text-sky-400" />
|
||||
<div class="relative">
|
||||
<input
|
||||
type="tel"
|
||||
name="customer_phone"
|
||||
id="customer_phone"
|
||||
value={@form[:customer_phone].value}
|
||||
class="w-full pl-11 pr-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-shadow"
|
||||
placeholder="+39 123456789"
|
||||
/>
|
||||
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
||||
<.icon name="hero-phone" class="size-5 text-slate-400 dark:text-slate-500" />
|
||||
</div>
|
||||
<span class="text-sm font-medium text-slate-700 dark:text-slate-300 truncate">
|
||||
{@current_user.phone_number}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.input
|
||||
field={@form[:customer_comment]}
|
||||
type="textarea"
|
||||
label="Comments (Optional)"
|
||||
placeholder="Any special requests or notes..."
|
||||
rows="4"
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
for="customer_comment"
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||
>
|
||||
Comments (Optional)
|
||||
</label>
|
||||
<textarea
|
||||
name="customer_comment"
|
||||
id="customer_comment"
|
||||
placeholder="Any special requests or notes..."
|
||||
rows="4"
|
||||
class="w-full px-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-shadow resize-none"
|
||||
>{@form[:customer_comment].value}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-sky-50 dark:bg-sky-900/20 border border-sky-200 dark:border-sky-800 rounded-xl">
|
||||
<div class="flex gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<.icon name="hero-information-circle" class="size-5 text-sky-600 dark:text-sky-400" />
|
||||
</div>
|
||||
<div class="text-xs text-slate-600 dark:text-slate-300 space-y-1">
|
||||
<ul class="list-disc list-inside space-y-0.5 ml-1">
|
||||
<li>Cancel anytime with no commitment</li>
|
||||
<li>Payment upon arrival only</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center gap-3">
|
||||
|
|
|
|||
|
|
@ -26,10 +26,14 @@ defmodule SpazioSolazzoWeb.ProfileLive do
|
|||
def handle_event("save_profile", %{"form" => form_params}, socket) do
|
||||
case Form.submit(socket.assigns.profile_form, params: form_params) do
|
||||
{:ok, updated_user} ->
|
||||
form =
|
||||
Accounts.form_to_update_profile(updated_user, actor: updated_user)
|
||||
|> to_form()
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:current_user, updated_user)
|
||||
|> assign(:profile_form, to_form(form_params))
|
||||
|> assign(:profile_form, form)
|
||||
|> put_flash(:info, "Profile updated successfully")}
|
||||
|
||||
{:error, form} ->
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@
|
|||
<.input
|
||||
field={@profile_form[:name]}
|
||||
type="text"
|
||||
label="Full Name"
|
||||
label="Full Name *"
|
||||
required
|
||||
placeholder="Enter your full name"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900/50 border-slate-200 dark:border-slate-700 rounded-xl px-4 py-3 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent transition-all outline-none"
|
||||
/>
|
||||
|
|
@ -63,7 +64,7 @@
|
|||
field={@profile_form[:phone_number]}
|
||||
type="tel"
|
||||
label="Phone Number"
|
||||
placeholder="+39"
|
||||
placeholder="+39 123456789"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900/50 border-slate-200 dark:border-slate-700 rounded-xl px-4 py-3 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent transition-all outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
defmodule SpazioSolazzo.Repo.Migrations.SetPhoneNumberAsNullable do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:users) do
|
||||
modify :phone_number, :text, null: true
|
||||
end
|
||||
|
||||
alter table(:bookings) do
|
||||
modify :customer_phone, :text, null: true
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:bookings) do
|
||||
modify :customer_phone, :text, null: false
|
||||
end
|
||||
|
||||
alter table(:users) do
|
||||
modify :phone_number, :text, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
244
priv/resource_snapshots/repo/bookings/20260115004442.json
Normal file
244
priv/resource_snapshots/repo/bookings/20260115004442.json
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "date",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_email",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "start_time",
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "end_time",
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_phone",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_comment",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "\"reserved\"",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "state",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "bookings_asset_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "assets"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "asset_id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "bookings_time_slot_template_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "time_slot_templates"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "time_slot_template_id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": true,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "bookings_user_id_fkey",
|
||||
"on_delete": "nilify",
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "users"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "user_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "1480C13D76AD8CE079362CC851CF250063914A40A6CA48182E3D3B5D83CD174A",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.SpazioSolazzo.Repo",
|
||||
"schema": null,
|
||||
"table": "bookings"
|
||||
}
|
||||
82
priv/resource_snapshots/repo/users/20260115004442.json
Normal file
82
priv/resource_snapshots/repo/users/20260115004442.json
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "email",
|
||||
"type": "citext"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "phone_number",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "6D20D84C076B09FEAFF7A68C69930AF48936A628BBEE432695F0ECC02B0F4EFA",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "users_unique_email_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "email"
|
||||
}
|
||||
],
|
||||
"name": "unique_email",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.SpazioSolazzo.Repo",
|
||||
"schema": null,
|
||||
"table": "users"
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
|
||||
describe "update_profile" do
|
||||
test "allows user to update their own name and phone_number" do
|
||||
user = create_test_user("test@example.com")
|
||||
user = register_user("test@example.com")
|
||||
|
||||
{:ok, updated_user} =
|
||||
Accounts.update_profile(user, "Updated Name", "+9876543210", actor: user)
|
||||
|
|
@ -20,8 +20,8 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
end
|
||||
|
||||
test "prevents user from updating another user's profile" do
|
||||
user1 = create_test_user("user1@example.com")
|
||||
user2 = create_test_user("user2@example.com")
|
||||
user1 = register_user("user1@example.com")
|
||||
user2 = register_user("user2@example.com")
|
||||
|
||||
result =
|
||||
Accounts.update_profile(user2, "Hacker", "1235837", actor: user1)
|
||||
|
|
@ -30,7 +30,7 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
end
|
||||
|
||||
test "validates that name is present" do
|
||||
user = create_test_user("test@example.com")
|
||||
user = register_user("test@example.com")
|
||||
|
||||
result =
|
||||
Accounts.update_profile(user, "", "+9876543210", actor: user)
|
||||
|
|
@ -42,7 +42,7 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
|
||||
describe "terminate_account with delete_history: false (anonymization)" do
|
||||
test "deletes user but preserves bookings with nullified user_id" do
|
||||
user = create_test_user("delete@example.com")
|
||||
user = register_user("delete@example.com")
|
||||
{_space, asset, time_slot} = create_booking_fixtures()
|
||||
|
||||
{:ok, booking1} =
|
||||
|
|
@ -87,7 +87,7 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
end
|
||||
|
||||
test "cancels future confirmed bookings before anonymizing" do
|
||||
user = create_test_user("cancel@example.com")
|
||||
user = register_user("cancel@example.com")
|
||||
{_space, asset, time_slot} = create_booking_fixtures()
|
||||
|
||||
future_date = Date.add(Date.utc_today(), 7)
|
||||
|
|
@ -133,7 +133,7 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
|
||||
describe "terminate_account with delete_history: true (hard delete)" do
|
||||
test "deletes user and all associated bookings permanently" do
|
||||
user = create_test_user("harddelete@example.com")
|
||||
user = register_user("harddelete@example.com")
|
||||
{_space, asset, time_slot} = create_booking_fixtures()
|
||||
|
||||
{:ok, booking1} =
|
||||
|
|
@ -173,8 +173,8 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
|
||||
describe "terminate_account authorization" do
|
||||
test "prevents user from deleting another user's account" do
|
||||
user1 = create_test_user("user1@example.com")
|
||||
user2 = create_test_user("user2@example.com")
|
||||
user1 = register_user("user1@example.com")
|
||||
user2 = register_user("user2@example.com")
|
||||
|
||||
result = Accounts.terminate_account(user2, false, actor: user1)
|
||||
|
||||
|
|
@ -182,18 +182,6 @@ defmodule SpazioSolazzo.Accounts.UserTest do
|
|||
end
|
||||
end
|
||||
|
||||
defp create_test_user(email) do
|
||||
{:ok, user} =
|
||||
SpazioSolazzo.Repo.insert(%User{
|
||||
id: Ash.UUID.generate(),
|
||||
email: email,
|
||||
name: "Test User",
|
||||
phone_number: "+1234567890"
|
||||
})
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
defp create_booking_fixtures do
|
||||
unique_id = :erlang.unique_integer([:positive, :monotonic])
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
alias SpazioSolazzo.BookingSystem.Booking
|
||||
alias SpazioSolazzo.Accounts.User
|
||||
|
||||
alias SpazioSolazzo.BookingSystem.Booking.EmailWorker
|
||||
|
||||
setup do
|
||||
|
|
@ -20,13 +18,7 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
space.id
|
||||
)
|
||||
|
||||
{:ok, user} =
|
||||
SpazioSolazzo.Repo.insert(%User{
|
||||
id: Ash.UUID.generate(),
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
phone_number: "+1234567890"
|
||||
})
|
||||
user = register_user("test@example.com")
|
||||
|
||||
%{space: space, asset: asset, time_slot: time_slot, user: user}
|
||||
end
|
||||
|
|
@ -236,7 +228,7 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
|
|||
# Load the booking with the user relationship
|
||||
{:ok, booking_with_user} = Ash.load(booking, :user, authorize?: false)
|
||||
assert booking_with_user.user.id == user.id
|
||||
assert to_string(booking_with_user.user.email) == user.email
|
||||
assert booking_with_user.user.email == user.email
|
||||
assert booking_with_user.user.name == user.name
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ defmodule SpazioSolazzoWeb.BookingControllerTest do
|
|||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
alias SpazioSolazzo.BookingSystem.Booking.Token
|
||||
alias SpazioSolazzo.Accounts.User
|
||||
|
||||
setup do
|
||||
unique_id = :erlang.unique_integer([:positive, :monotonic])
|
||||
|
|
@ -21,13 +20,7 @@ defmodule SpazioSolazzoWeb.BookingControllerTest do
|
|||
space.id
|
||||
)
|
||||
|
||||
{:ok, user} =
|
||||
SpazioSolazzo.Repo.insert(%User{
|
||||
id: Ash.UUID.generate(),
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
phone_number: "+1234567890"
|
||||
})
|
||||
user = register_user("test@example.com", "Test User", "+1234567890")
|
||||
|
||||
%{space: space, asset: asset, time_slot: time_slot, user: user}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ defmodule SpazioSolazzoWeb.BookingLive.AssetBookingTest do
|
|||
import Phoenix.LiveViewTest
|
||||
|
||||
alias SpazioSolazzo.BookingSystem
|
||||
alias SpazioSolazzo.Accounts.User
|
||||
|
||||
setup %{conn: conn} do
|
||||
{:ok, space} = BookingSystem.create_space("TestSpace", "test-space", "Test description")
|
||||
|
|
@ -20,28 +19,12 @@ defmodule SpazioSolazzoWeb.BookingLive.AssetBookingTest do
|
|||
space.id
|
||||
)
|
||||
|
||||
conn = Plug.Test.init_test_session(conn, %{})
|
||||
conn = log_in_user(conn)
|
||||
user = register_user("test@example.com", "Test User", "+1234567890")
|
||||
conn = log_in_user(conn, user)
|
||||
|
||||
%{space: space, asset: asset, slot: slot, conn: conn}
|
||||
end
|
||||
|
||||
defp log_in_user(conn) do
|
||||
{:ok, user} =
|
||||
SpazioSolazzo.Repo.insert(%User{
|
||||
id: Ash.UUID.generate(),
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
phone_number: "+1234567890"
|
||||
})
|
||||
|
||||
{:ok, token, _claims} = AshAuthentication.Jwt.token_for_user(user)
|
||||
|
||||
conn
|
||||
|> Plug.Test.init_test_session(%{})
|
||||
|> Plug.Conn.put_session("user_token", token)
|
||||
end
|
||||
|
||||
describe "AssetBooking mount" do
|
||||
test "renders asset booking page with available time slots", %{
|
||||
conn: conn,
|
||||
|
|
@ -105,7 +88,11 @@ defmodule SpazioSolazzoWeb.BookingLive.AssetBookingTest do
|
|||
|
||||
view
|
||||
|> element("#booking-form")
|
||||
|> render_submit(%{"customer_comment" => "test comment"})
|
||||
|> render_submit(%{
|
||||
"customer_name" => "Test User",
|
||||
"customer_phone" => "+1234567890",
|
||||
"customer_comment" => "test comment"
|
||||
})
|
||||
|
||||
assert has_element?(view, "#success-modal")
|
||||
|
||||
|
|
@ -224,4 +211,113 @@ defmodule SpazioSolazzoWeb.BookingLive.AssetBookingTest do
|
|||
assert html =~ ~s(<div class="p-2"></div>)
|
||||
end
|
||||
end
|
||||
|
||||
describe "AssetBooking without phone number" do
|
||||
setup %{conn: conn} do
|
||||
# Create a separate connection with a user without phone number
|
||||
user = register_user("nophone@example.com", "User Without Phone", nil)
|
||||
conn = log_in_user(conn, user)
|
||||
|
||||
%{conn: conn}
|
||||
end
|
||||
|
||||
test "user without phone number can view booking form", %{conn: conn, asset: asset} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#booking-modal")
|
||||
assert has_element?(view, "input[name='customer_name']")
|
||||
assert has_element?(view, "input[name='customer_phone']")
|
||||
assert has_element?(view, "textarea[name='customer_comment']")
|
||||
end
|
||||
|
||||
test "user without phone number can create booking without providing phone", %{
|
||||
conn: conn,
|
||||
asset: asset
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#booking-modal")
|
||||
|
||||
# Submit booking with name but no phone
|
||||
view
|
||||
|> element("#booking-form")
|
||||
|> render_submit(%{
|
||||
"customer_name" => "User Without Phone",
|
||||
"customer_phone" => "",
|
||||
"customer_comment" => "test comment"
|
||||
})
|
||||
|
||||
assert has_element?(view, "#success-modal")
|
||||
|
||||
assert {:ok, [booking]} =
|
||||
BookingSystem.list_active_asset_bookings_by_date(asset.id, Date.utc_today())
|
||||
|
||||
assert booking.customer_email == "nophone@example.com"
|
||||
assert booking.customer_name == "User Without Phone"
|
||||
assert booking.customer_phone == nil or booking.customer_phone == ""
|
||||
assert booking.customer_comment == "test comment"
|
||||
end
|
||||
|
||||
test "user without phone number can edit name in booking form", %{
|
||||
conn: conn,
|
||||
asset: asset
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot']")
|
||||
|> render_click()
|
||||
|
||||
# Change the name
|
||||
view
|
||||
|> element("#booking-form")
|
||||
|> render_submit(%{
|
||||
"customer_name" => "Different Name",
|
||||
"customer_phone" => "",
|
||||
"customer_comment" => ""
|
||||
})
|
||||
|
||||
assert has_element?(view, "#success-modal")
|
||||
|
||||
assert {:ok, [booking]} =
|
||||
BookingSystem.list_active_asset_bookings_by_date(asset.id, Date.utc_today())
|
||||
|
||||
assert booking.customer_name == "Different Name"
|
||||
end
|
||||
|
||||
test "user without phone number can optionally add phone during booking", %{
|
||||
conn: conn,
|
||||
asset: asset
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/book/asset/#{asset.id}")
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_slot']")
|
||||
|> render_click()
|
||||
|
||||
# Add phone number during booking
|
||||
view
|
||||
|> element("#booking-form")
|
||||
|> render_submit(%{
|
||||
"customer_name" => "User Without Phone",
|
||||
"customer_phone" => "+39 123 456 789",
|
||||
"customer_comment" => ""
|
||||
})
|
||||
|
||||
assert has_element?(view, "#success-modal")
|
||||
|
||||
assert {:ok, [booking]} =
|
||||
BookingSystem.list_active_asset_bookings_by_date(asset.id, Date.utc_today())
|
||||
|
||||
assert booking.customer_phone == "+39 123 456 789"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ defmodule SpazioSolazzoWeb.ProfileLiveTest do
|
|||
alias SpazioSolazzo.BookingSystem.Booking
|
||||
|
||||
setup %{conn: conn} do
|
||||
conn = Plug.Test.init_test_session(conn, %{})
|
||||
{conn, user} = log_in_user(conn)
|
||||
user = register_user("test@example.com", "Test User", "+123456789")
|
||||
conn = log_in_user(conn, user)
|
||||
|
||||
%{user: user, conn: conn}
|
||||
end
|
||||
|
||||
|
|
@ -191,25 +192,6 @@ defmodule SpazioSolazzoWeb.ProfileLiveTest do
|
|||
end
|
||||
end
|
||||
|
||||
defp log_in_user(conn) do
|
||||
user =
|
||||
SpazioSolazzo.Repo.insert!(%User{
|
||||
id: Ash.UUID.generate(),
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
phone_number: "+1234567890"
|
||||
})
|
||||
|
||||
{:ok, token, _claims} = AshAuthentication.Jwt.token_for_user(user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> Plug.Test.init_test_session(%{})
|
||||
|> Plug.Conn.put_session("user_token", token)
|
||||
|
||||
{conn, user}
|
||||
end
|
||||
|
||||
defp create_booking_fixtures do
|
||||
unique_id = :erlang.unique_integer([:positive, :monotonic])
|
||||
|
||||
|
|
|
|||
64
test/support/auth_helpers.ex
Normal file
64
test/support/auth_helpers.ex
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
defmodule SpazioSolazzo.AuthHelpers do
|
||||
@moduledoc """
|
||||
Authentication helper functions for tests.
|
||||
|
||||
Provides utilities to create and authenticate users via the magic link flow,
|
||||
simulating realistic authentication behavior in tests.
|
||||
"""
|
||||
|
||||
alias SpazioSolazzo.Accounts
|
||||
|
||||
@doc """
|
||||
Creates a test session and logs the user into it.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `conn` - The test connection
|
||||
- `user` - User to log in
|
||||
|
||||
## Examples
|
||||
|
||||
conn = log_in_user(conn, user)
|
||||
"""
|
||||
def log_in_user(conn, user) do
|
||||
conn
|
||||
|> Plug.Test.init_test_session(%{})
|
||||
|> AshAuthentication.Phoenix.Plug.store_in_session(user)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a user via magic link authentication without attaching to a connection.
|
||||
|
||||
Useful for tests that need a user object but don't need an authenticated connection.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `email` - User's email address
|
||||
- `name` - Optional user's full name (defaults to "Test User")
|
||||
- `phone_number` - Optional phone number (defaults to nil)
|
||||
|
||||
## Examples
|
||||
|
||||
user = register_user("test@example.com", "Test User", "+1234567890")
|
||||
user = register_user("user@example.com", "User Name")
|
||||
user = register_user("user@example.com")
|
||||
"""
|
||||
def register_user(email, name \\ "Test user", phone_number \\ nil) do
|
||||
strategy = AshAuthentication.Info.strategy!(SpazioSolazzo.Accounts.User, :magic_link)
|
||||
|
||||
{:ok, token} =
|
||||
AshAuthentication.Strategy.MagicLink.request_token_for_identity(strategy, email)
|
||||
|
||||
# Sign in with magic link
|
||||
{:ok, user} =
|
||||
Accounts.sign_in_with_magic_link(
|
||||
token,
|
||||
false,
|
||||
name,
|
||||
phone_number,
|
||||
authorize?: false
|
||||
)
|
||||
|
||||
user
|
||||
end
|
||||
end
|
||||
|
|
@ -31,6 +31,9 @@ defmodule SpazioSolazzoWeb.ConnCase do
|
|||
|
||||
# Import DataCase helpers for email testing
|
||||
import SpazioSolazzo.DataCase, only: [pop_email: 0, pop_email: 2]
|
||||
|
||||
# Import authentication helpers
|
||||
import SpazioSolazzo.AuthHelpers
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ defmodule SpazioSolazzo.DataCase do
|
|||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
import SpazioSolazzo.DataCase
|
||||
import SpazioSolazzo.AuthHelpers
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue