Critical fixes: - Renamed controller actions to follow Phoenix conventions (verify→new, create→verify) - Added sudo mode check in GET action using last_sudo_at timestamp - Added TOTP enrollment check in GET action with redirect - Changed default return path from /orgs to /users/settings - Updated route paths from /users/sudo/verify to /users/sudo-verify (dash separator) - Added success flash message "Identity verified." Implementation details: - new/2 (GET): Checks recently_verified_sudo?/1 helper that examines last_sudo_at instead of authenticated_at to distinguish sudo verification from regular login - verify/2 (POST): Adds info flash and defaults to /users/settings - recently_verified_sudo?/1: Private helper checking last_sudo_at within 10 minutes - Updated all routes, templates, and tests to use new paths and action names Tests: - Added tests for already-in-sudo-mode redirect behavior - Added test for TOTP enrollment requirement - Updated all test assertions for new route paths and success messages - All 15 controller tests passing - Full test suite passing (4076 tests, 0 failures)
220 lines
7.2 KiB
Elixir
220 lines
7.2 KiB
Elixir
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
|