defmodule ToweropsWeb.UserSudoControllerTest do use ToweropsWeb.ConnCase, async: true import Towerops.AccountsFixtures alias Towerops.Accounts setup do # Create user with TOTP enabled (required for sudo mode) user = user_fixture(enable_totp: true) %{user: user} end describe "GET /users/sudo-verify" do test "renders sudo verification page for authenticated user", %{conn: conn, user: user} do conn = conn |> log_in_user(user) |> get(~p"/users/sudo-verify") response = html_response(conn, 200) assert response =~ "Re-authenticate" assert response =~ "Enter the 6-digit code from your authenticator app" end test "redirects unauthenticated users to login", %{conn: conn} do conn = get(conn, ~p"/users/sudo-verify") assert redirected_to(conn) == ~p"/users/log-in" assert Phoenix.Flash.get(conn.assigns.flash, :error) == "You must log in to access this page." end test "preserves return_to path from session", %{conn: conn, user: user} do conn = conn |> log_in_user(user) |> init_test_session(user_return_to: "/users/settings") |> get(~p"/users/sudo-verify") assert html_response(conn, 200) =~ "Re-authenticate" assert get_session(conn, :user_return_to) == "/users/settings" end test "redirects to destination if already in sudo mode", %{conn: conn, user: user} do # Grant sudo mode first {:ok, _user} = Accounts.grant_sudo_mode(user) conn = conn |> log_in_user(user) |> init_test_session(user_return_to: "/users/settings") |> get(~p"/users/sudo-verify") assert redirected_to(conn) == "/users/settings" assert get_session(conn, :user_return_to) == nil end test "redirects to /users/settings if already in sudo mode and no return_to", %{ conn: conn, user: user } do # Grant sudo mode first {:ok, _user} = Accounts.grant_sudo_mode(user) conn = conn |> log_in_user(user) |> get(~p"/users/sudo-verify") assert redirected_to(conn) == ~p"/users/settings" end test "redirects to TOTP enrollment if user has no TOTP", %{conn: conn} do # Create user without TOTP user_no_totp = user_fixture(enable_totp: false) conn = conn |> log_in_user(user_no_totp) |> get(~p"/users/sudo-verify") assert redirected_to(conn) == ~p"/account/totp-enrollment" assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Two-factor authentication is required for this action." end end describe "POST /users/sudo-verify" do test "grants sudo mode and redirects on valid TOTP code", %{conn: conn, user: user} do # Get valid TOTP code for user code = NimbleTOTP.verification_code(user.totp_secret) conn = conn |> log_in_user(user) |> init_test_session(user_return_to: "/users/settings") |> post(~p"/users/sudo-verify", %{"user" => %{"totp_code" => code}}) # Should redirect to return_to path (implies sudo mode was granted) assert redirected_to(conn) == "/users/settings" # Should show success flash message assert Phoenix.Flash.get(conn.assigns.flash, :info) == "Identity verified." # Should clear return_to from session assert get_session(conn, :user_return_to) == nil # Should update last_sudo_at timestamp updated_user = Accounts.get_user!(user.id) assert updated_user.last_sudo_at assert DateTime.diff(updated_user.last_sudo_at, DateTime.utc_now(), :second) <= 1 end test "grants sudo mode and redirects to /users/settings if no return_to", %{ conn: conn, user: user } do code = NimbleTOTP.verification_code(user.totp_secret) conn = conn |> log_in_user(user) |> post(~p"/users/sudo-verify", %{"user" => %{"totp_code" => code}}) # Should redirect to /users/settings (default) assert redirected_to(conn) == ~p"/users/settings" # Should show success flash message assert Phoenix.Flash.get(conn.assigns.flash, :info) == "Identity verified." # Should update last_sudo_at timestamp updated_user = Accounts.get_user!(user.id) assert updated_user.last_sudo_at assert DateTime.diff(updated_user.last_sudo_at, DateTime.utc_now(), :second) <= 1 end test "shows error on invalid TOTP code", %{conn: conn, user: user} do conn = conn |> log_in_user(user) |> post(~p"/users/sudo-verify", %{"user" => %{"totp_code" => "000000"}}) response = html_response(conn, 200) assert response =~ "Invalid authentication code" assert response =~ "Re-authenticate" # Should not grant sudo mode user = Accounts.get_user!(user.id) refute Accounts.sudo_mode?(user) end test "shows error when recovery code is attempted", %{conn: conn, user: user} do conn = conn |> log_in_user(user) |> post(~p"/users/sudo-verify", %{"user" => %{"totp_code" => "ABCD-EFGH"}}) response = html_response(conn, 200) assert response =~ "Recovery codes are not allowed for sudo mode verification" assert response =~ "Re-authenticate" # Should not grant sudo mode user = Accounts.get_user!(user.id) refute Accounts.sudo_mode?(user) end test "shows error when TOTP code is too long", %{conn: conn, user: user} do conn = conn |> log_in_user(user) |> post(~p"/users/sudo-verify", %{"user" => %{"totp_code" => "1234567"}}) response = html_response(conn, 200) assert response =~ "Recovery codes are not allowed for sudo mode verification" assert response =~ "Re-authenticate" end test "shows error when TOTP code contains letters", %{conn: conn, user: user} do conn = conn |> log_in_user(user) |> post(~p"/users/sudo-verify", %{"user" => %{"totp_code" => "12AB34"}}) response = html_response(conn, 200) assert response =~ "Recovery codes are not allowed for sudo mode verification" assert response =~ "Re-authenticate" end test "redirects unauthenticated users to login", %{conn: conn} do conn = post(conn, ~p"/users/sudo-verify", %{"user" => %{"totp_code" => "123456"}}) assert redirected_to(conn) == ~p"/users/log-in" assert Phoenix.Flash.get(conn.assigns.flash, :error) == "You must log in to access this page." end test "preserves return_to path on verification failure", %{conn: conn, user: user} do conn = conn |> log_in_user(user) |> init_test_session(user_return_to: "/users/settings") |> post(~p"/users/sudo-verify", %{"user" => %{"totp_code" => "000000"}}) assert html_response(conn, 200) =~ "Invalid authentication code" # return_to should be preserved for next attempt assert get_session(conn, :user_return_to) == "/users/settings" end test "handles missing TOTP code parameter", %{conn: conn, user: user} do conn = conn |> log_in_user(user) |> post(~p"/users/sudo-verify", %{"user" => %{}}) response = html_response(conn, 200) assert response =~ "Re-authenticate" # Should render form again (no crash) end end end