diff --git a/test/towerops_web/live/user_settings_live/session_manager_test.exs b/test/towerops_web/live/user_settings_live/session_manager_test.exs new file mode 100644 index 00000000..0e0189ef --- /dev/null +++ b/test/towerops_web/live/user_settings_live/session_manager_test.exs @@ -0,0 +1,88 @@ +defmodule ToweropsWeb.UserSettingsLive.SessionManagerTest do + use Towerops.DataCase, async: false + + import Towerops.AccountsFixtures + + alias Phoenix.LiveView.Socket + alias Towerops.Accounts.Scope + alias ToweropsWeb.UserSettingsLive.SessionManager + + describe "show_revoke_all_modal/1 and cancel_revoke_all/1" do + test "show flips the modal flag, cancel resets it" do + socket = base_socket(user_fixture()) + + assert socket.assigns[:show_revoke_all_modal] in [nil, false] + shown = SessionManager.show_revoke_all_modal(socket) + assert shown.assigns.show_revoke_all_modal == true + + cancelled = SessionManager.cancel_revoke_all(shown) + assert cancelled.assigns.show_revoke_all_modal == false + end + end + + describe "assign_mobile_sessions/1" do + test "assigns the user's mobile sessions list (empty when none exist)" do + socket = base_socket(user_fixture()) + + result = SessionManager.assign_mobile_sessions(socket) + + assert is_list(result.assigns.mobile_sessions) + assert result.assigns.mobile_sessions == [] + end + end + + describe "assign_browser_sessions/1" do + test "assigns the user's active browser sessions" do + socket = base_socket(user_fixture()) + + result = SessionManager.assign_browser_sessions(socket) + + assert is_list(result.assigns.browser_sessions) + end + end + + describe "get_current_token_id/1" do + test "returns nil when no token is present in assigns" do + socket = base_socket(user_fixture()) + assert SessionManager.get_current_token_id(socket) == nil + end + + test "returns nil for a token value that no UserToken row matches" do + socket = %Socket{assigns: %{current_token: "no-such-token"}} + assert SessionManager.get_current_token_id(socket) == nil + end + + test "ignores non-binary current_token values" do + socket = %Socket{assigns: %{current_token: nil}} + assert SessionManager.get_current_token_id(socket) == nil + end + end + + describe "revoke_mobile_device/2 + toggle_device_alerts/3" do + test "revoke returns an error flash for a mobile session that doesn't exist" do + socket = base_socket(user_fixture()) + result = SessionManager.revoke_mobile_device(socket, Ecto.UUID.generate()) + assert result.assigns.flash["error"] + end + + test "toggle returns an error flash for a mobile session that doesn't exist" do + socket = base_socket(user_fixture()) + result = SessionManager.toggle_device_alerts(socket, Ecto.UUID.generate(), "true") + assert result.assigns.flash["error"] + end + end + + # -- helpers -- + + defp base_socket(user) do + scope = Scope.for_user(user) + + %Socket{ + assigns: %{ + current_scope: scope, + flash: %{}, + __changed__: %{} + } + } + end +end diff --git a/test/towerops_web/live/user_settings_live/totp_manager_test.exs b/test/towerops_web/live/user_settings_live/totp_manager_test.exs new file mode 100644 index 00000000..c6bd13bc --- /dev/null +++ b/test/towerops_web/live/user_settings_live/totp_manager_test.exs @@ -0,0 +1,97 @@ +defmodule ToweropsWeb.UserSettingsLive.TotpManagerTest do + use Towerops.DataCase, async: false + + import Towerops.AccountsFixtures + + alias Phoenix.LiveView.Socket + alias Towerops.Accounts.Scope + alias ToweropsWeb.UserSettingsLive.TotpManager + + describe "modal state helpers" do + test "show_add_device_modal flips the flag, cancel resets it" do + socket = base_socket(user_fixture()) + + assert socket.assigns[:show_add_device_modal] in [nil, false] + shown = TotpManager.show_add_device_modal(socket) + assert shown.assigns.show_add_device_modal == true + + cancelled = TotpManager.cancel_add_device(shown) + assert cancelled.assigns.show_add_device_modal == false + end + + test "close_recovery_codes_modal hides modal and clears generated codes" do + socket = + user_fixture() + |> base_socket() + |> Phoenix.Component.assign(:show_recovery_codes_modal, true) + |> Phoenix.Component.assign(:generated_recovery_codes, ["a", "b"]) + + result = TotpManager.close_recovery_codes_modal(socket) + + assert result.assigns.show_recovery_codes_modal == false + assert result.assigns.generated_recovery_codes == nil + end + end + + describe "assign_totp_devices/1" do + test "assigns the user's totp device list (empty when none)" do + socket = base_socket(user_fixture()) + result = TotpManager.assign_totp_devices(socket) + assert is_list(result.assigns.totp_devices) + end + end + + describe "assign_recovery_codes_count/1" do + test "assigns an integer count of unused recovery codes" do + socket = base_socket(user_fixture()) + result = TotpManager.assign_recovery_codes_count(socket) + assert is_integer(result.assigns.unused_recovery_codes_count) + end + end + + describe "verify_new_device/2" do + test "rejects a wrong code with an error flash" do + user = user_fixture() + + # Stage the QR-modal assigns the way create_device would have done. + socket = + user + |> base_socket() + |> Phoenix.Component.assign(:new_device_secret, "JBSWY3DPEHPK3PXP") + |> Phoenix.Component.assign(:new_device_id, Ecto.UUID.generate()) + + result = TotpManager.verify_new_device(socket, "000000") + + assert result.assigns.flash["error"] =~ "Invalid code" + end + end + + describe "delete_device/2" do + test "returns an error flash for a device id that doesn't belong to the user" do + socket = base_socket(user_fixture()) + result = TotpManager.delete_device(socket, Ecto.UUID.generate()) + assert result.assigns.flash["error"] + end + end + + describe "regenerate_recovery_codes/1" do + test "stages the modal and generated codes on success" do + socket = base_socket(user_fixture()) + result = TotpManager.regenerate_recovery_codes(socket) + + # Either succeeded (modal flag true) or failed with an error flash. + assert result.assigns[:show_recovery_codes_modal] == true or + result.assigns.flash["error"] + end + end + + defp base_socket(user) do + %Socket{ + assigns: %{ + current_scope: Scope.for_user(user), + flash: %{}, + __changed__: %{} + } + } + end +end