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