335 lines
12 KiB
Elixir
335 lines
12 KiB
Elixir
defmodule ToweropsWeb.UserSettingsLiveTest do
|
|
use ToweropsWeb.ConnCase, async: true
|
|
|
|
import Phoenix.LiveViewTest
|
|
import Towerops.AccountsFixtures
|
|
|
|
alias Towerops.Accounts
|
|
|
|
describe "UserSettingsLive with sudo mode" do
|
|
setup :register_and_log_in_user_with_sudo
|
|
|
|
test "renders settings page", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/users/settings")
|
|
|
|
assert html =~ "Account Settings"
|
|
assert html =~ "Personal Information"
|
|
end
|
|
|
|
test "allows updating profile", %{conn: conn, user: user} do
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=personal")
|
|
|
|
# Update profile
|
|
result =
|
|
view
|
|
|> form("#update_profile", user: %{first_name: "John", last_name: "Doe"})
|
|
|> render_submit()
|
|
|
|
assert result =~ "Profile updated successfully"
|
|
|
|
# Verify database was updated
|
|
updated_user = Accounts.get_user!(user.id)
|
|
assert updated_user.first_name == "John"
|
|
assert updated_user.last_name == "Doe"
|
|
end
|
|
|
|
test "allows updating email", %{conn: conn, user: user} do
|
|
new_email = unique_user_email()
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=account")
|
|
|
|
# Update email
|
|
result =
|
|
view
|
|
|> form("#update_email", user: %{email: new_email})
|
|
|> render_submit()
|
|
|
|
assert result =~ "A link to confirm your email"
|
|
|
|
# Old email should still be in use
|
|
assert Accounts.get_user_by_email(user.email)
|
|
end
|
|
|
|
test "allows updating password", %{conn: conn} do
|
|
new_password = valid_user_password()
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=account")
|
|
|
|
# Update password - this will log the user out
|
|
assert {:error, {:redirect, %{to: "/users/log-in"}}} =
|
|
view
|
|
|> form("#update_password",
|
|
user: %{
|
|
password: new_password,
|
|
password_confirmation: new_password
|
|
}
|
|
)
|
|
|> render_submit()
|
|
end
|
|
|
|
test "shows error for mismatched password confirmation", %{conn: conn} do
|
|
new_password = valid_user_password()
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=account")
|
|
|
|
# Try to update password with non-matching confirmation - page stays on form
|
|
html =
|
|
view
|
|
|> form("#update_password",
|
|
user: %{
|
|
password: new_password,
|
|
password_confirmation: "different_password"
|
|
}
|
|
)
|
|
|> render_submit()
|
|
|
|
# Should still be on the settings page with the form visible
|
|
assert html =~ "Account Settings"
|
|
assert html =~ "Change Password"
|
|
end
|
|
|
|
test "shows tabs navigation", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/users/settings")
|
|
|
|
# Check for tab navigation
|
|
assert html =~ "Personal"
|
|
assert html =~ "Account"
|
|
assert html =~ "Security"
|
|
assert html =~ "API"
|
|
assert html =~ "Sessions"
|
|
assert html =~ "Notifications"
|
|
end
|
|
|
|
test "allows switching tabs via params", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/users/settings?tab=security")
|
|
|
|
assert html =~ "Account Settings"
|
|
# Security tab should be active (would need to check for active class in real template)
|
|
end
|
|
end
|
|
|
|
describe "UserSettingsLive TOTP management with sudo mode" do
|
|
setup :register_and_log_in_user_with_sudo
|
|
|
|
test "shows enrolled TOTP devices", %{conn: conn, user: user} do
|
|
# Create a TOTP device
|
|
Accounts.create_totp_device(user.id, "Test Authenticator")
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/users/settings?tab=security")
|
|
|
|
assert html =~ "Test Authenticator"
|
|
end
|
|
|
|
test "shows multiple TOTP devices", %{conn: conn, user: user} do
|
|
# Create two TOTP devices
|
|
{:ok, _device1, _secret1} = Accounts.create_totp_device(user.id, "Device 1")
|
|
{:ok, _device2, _secret2} = Accounts.create_totp_device(user.id, "Device 2")
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/users/settings?tab=security")
|
|
|
|
# Verify both devices are shown
|
|
assert html =~ "Device 1"
|
|
assert html =~ "Device 2"
|
|
end
|
|
end
|
|
|
|
describe "UserSettingsLive sessions with sudo mode" do
|
|
setup :register_and_log_in_user_with_sudo
|
|
|
|
test "shows active sessions", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/users/settings?tab=sessions")
|
|
|
|
assert html =~ "Active Sessions"
|
|
assert html =~ "Login History"
|
|
end
|
|
|
|
test "opens and cancels revoke all modal", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=sessions")
|
|
|
|
# The "Revoke All Other Sessions" button only renders when there are 2+ active
|
|
# browser sessions for the user (UX: nothing to revoke if you only have one).
|
|
# The test fixture creates a single session, so the trigger button isn't in the
|
|
# DOM. We exercise the modal-open/cancel handler logic directly here.
|
|
html = render_click(view, "show_revoke_all_modal")
|
|
assert html =~ "Revoke All"
|
|
|
|
html = render_click(view, "cancel_revoke_all")
|
|
refute html =~ "Are you sure"
|
|
end
|
|
|
|
test "shows mobile devices section on notifications tab", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/users/settings?tab=notifications")
|
|
|
|
assert html =~ "Mobile Devices"
|
|
end
|
|
end
|
|
|
|
describe "UserSettingsLive API tokens with sudo mode" do
|
|
setup :register_and_log_in_user_with_sudo
|
|
|
|
test "shows API tokens tab", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/users/settings?tab=api")
|
|
|
|
assert html =~ "API Tokens"
|
|
end
|
|
|
|
test "opens add token modal", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=api")
|
|
|
|
html = view |> element("#show-add-token-modal-empty") |> render_click()
|
|
|
|
assert html =~ "Create API Token"
|
|
end
|
|
|
|
test "cancels add token modal", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=api")
|
|
|
|
view |> element("#show-add-token-modal-empty") |> render_click()
|
|
html = view |> element("#cancel-add-token") |> render_click()
|
|
|
|
refute html =~ "Create API Token"
|
|
end
|
|
end
|
|
|
|
describe "UserSettingsLive login history with sudo mode" do
|
|
setup :register_and_log_in_user_with_sudo
|
|
|
|
test "shows login history", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/users/settings?tab=sessions")
|
|
|
|
assert html =~ "Login History"
|
|
end
|
|
|
|
test "paginates login history", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/users/settings?tab=sessions&page=1")
|
|
|
|
assert html =~ "Login History"
|
|
end
|
|
end
|
|
|
|
describe "UserSettingsLive API token creation with sudo mode" do
|
|
setup :register_and_log_in_user_with_sudo
|
|
|
|
test "creates API token and shows it", %{conn: conn, user: user} do
|
|
{:ok, org} = Towerops.Organizations.create_organization(%{name: "Token Org"}, user.id)
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=api")
|
|
|
|
view |> element("#show-add-token-modal-empty") |> render_click()
|
|
|
|
html =
|
|
view
|
|
|> form("#create-api-token-form", %{"name" => "My Token", "organization_id" => org.id})
|
|
|> render_submit()
|
|
|
|
assert html =~ "My Token" or html =~ "API Tokens"
|
|
end
|
|
|
|
test "create_api_token without name shows error", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=api")
|
|
|
|
# Defensive handler test: the UI marks `name` as `required`, so a real browser
|
|
# blocks empty submission. The handler still has to defend against tampered POSTs
|
|
# — so we drive the event directly to verify the server-side guard.
|
|
html = render_click(view, "create_api_token", %{})
|
|
assert html =~ "required" or html =~ "API Tokens"
|
|
end
|
|
|
|
test "close_token_modal clears token", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=api")
|
|
|
|
# The Close button only renders when the token-display modal is open (which only
|
|
# happens after a token is successfully created). This tests the handler's idempotent
|
|
# behavior when the close event fires without the modal being open.
|
|
html = render_click(view, "close_token_modal")
|
|
assert html =~ "API Tokens"
|
|
end
|
|
end
|
|
|
|
describe "UserSettingsLive session management with sudo mode" do
|
|
setup :register_and_log_in_user_with_sudo
|
|
|
|
test "shows security tab with add device modal", %{conn: conn} do
|
|
# show_add_device_modal is on the SECURITY tab (TOTP authenticator devices),
|
|
# not notifications. Original test mounted ?tab=notifications and only worked
|
|
# because direct render_click bypassed tab gating.
|
|
# The fixture user has enable_totp: true (one TOTP device), so the
|
|
# "with-devices" Add button renders, not the empty-state one.
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=security")
|
|
|
|
html = view |> element("#show-add-device-modal") |> render_click()
|
|
assert html =~ "Device" or html =~ "authenticator"
|
|
|
|
html = view |> element("#cancel-add-device") |> render_click()
|
|
assert html =~ "TOTP" or html =~ "authenticator" or html =~ "Devices"
|
|
end
|
|
end
|
|
|
|
describe "UserSettingsLive password breach check with sudo mode" do
|
|
setup :register_and_log_in_user_with_sudo
|
|
|
|
test "check_password_breach with empty value", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=account")
|
|
|
|
# Fired by phx-blur on #new-password — render_blur drives that path through the
|
|
# actual element, validating the wiring. Original test used render_click which
|
|
# silently bypassed the blur attribute.
|
|
html = view |> element("#new-password") |> render_blur(%{"value" => ""})
|
|
assert html =~ "Account Settings"
|
|
end
|
|
|
|
test "check_password_breach with no value param", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=account")
|
|
|
|
# Defensive handler test: the phx-blur event always carries the input value, so
|
|
# an empty params map can't be produced from the UI. Drive directly to verify
|
|
# the no-value branch of the handler.
|
|
html = render_click(view, "check_password_breach", %{})
|
|
assert html =~ "Account Settings"
|
|
end
|
|
|
|
test "check_password_breach with a value drives the HIBP path", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=account")
|
|
|
|
# Submit a password to the breach check. We don't care whether the
|
|
# response is :ok / count / :unknown — the handler should not crash.
|
|
html = view |> element("#new-password") |> render_blur(%{"value" => "Sup3rS3cretP@ssw0rd1!"})
|
|
assert html =~ "Account Settings"
|
|
end
|
|
end
|
|
|
|
describe "UserSettingsLive update_email error path" do
|
|
setup :register_and_log_in_user_with_sudo
|
|
|
|
test "update_email with invalid params keeps the form on the page", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=account")
|
|
|
|
# Submit invalid (empty) email + missing current password — Accounts.change_user_email
|
|
# produces an invalid changeset and the handler should re-render the form.
|
|
html =
|
|
render_change(view, "update_email", %{
|
|
"user" => %{"email" => "", "current_password" => ""}
|
|
})
|
|
|
|
assert html =~ "Account Settings"
|
|
end
|
|
end
|
|
|
|
describe "UserSettingsLive update_password error path" do
|
|
setup :register_and_log_in_user_with_sudo
|
|
|
|
test "update_password with mismatched confirmation re-renders the form", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=account")
|
|
|
|
html =
|
|
render_submit(view, "update_password", %{
|
|
"user" => %{
|
|
"password" => "newpassword123",
|
|
"password_confirmation" => "different123",
|
|
"current_password" => "wrong"
|
|
}
|
|
})
|
|
|
|
# The re-render leaves us on the same tab; the changeset error appears
|
|
# inline. Either flash or form helper text contains "Account Settings".
|
|
assert html =~ "Account Settings"
|
|
end
|
|
end
|
|
end
|