write tests & update UI

This commit is contained in:
JasterV 2026-01-15 11:52:09 +01:00
parent b7e47f1093
commit e23ddfd10e
12 changed files with 279 additions and 114 deletions

View file

@ -106,14 +106,15 @@
name="phone_number"
id="phone_number"
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"
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>
<!-- TODO: Add a text here letting the user know that their phone number won't be used for any commercial purposes -->
<!-- but only to contact them personally in case of any problems with a booking -->
</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">

View file

@ -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

View file

@ -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,12 +27,16 @@ 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
# TODO: Make name and phone fields editable
def render(assigns) do
~H"""
<div>
@ -54,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>
@ -82,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">

View file

@ -64,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>

View file

@ -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])

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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])

View 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

View file

@ -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

View file

@ -26,6 +26,7 @@ defmodule SpazioSolazzo.DataCase do
import Ecto.Changeset
import Ecto.Query
import SpazioSolazzo.DataCase
import SpazioSolazzo.AuthHelpers
end
end