towerops/test/towerops_web/live/user_settings_live_test.exs
Graham McIntire 9094f42098
feat: add Dell PowerVault storage array text-based sensor parsing
Implement vendor module for Dell PowerVault storage arrays that parse
text-based sensor messages from FCMGMT-MIB instead of structured tables.

Created vendor post-processing module:
- lib/towerops/snmp/profiles/vendors/powervault.ex
  * Walks FCMGMT-MIB::connUnitSensorMessage (OID 1.3.6.1.3.94.1.8.1.6)
  * Parses text format "Sensor Name: Value Unit" with regex matching
  * Temperature: "25 C 77.0F" → 25°C (extracts Celsius)
  * Voltage: "12.1V" → 12.1V
  * Current: "0.5A" → 0.5A
  * Battery Charge: "95%" → 95% (with threshold limits)

Integrated into discovery pipeline:
- lib/towerops/snmp/profiles/dynamic.ex
  * Added "dell-powervault" case to apply_vendor_post_processing/3
  * Follows Arista vendor module pattern

Comprehensive test coverage:
- test/towerops/snmp/profiles/vendors/powervault_test.exs
  * 21 tests covering all sensor types and edge cases
  * Tests multi-sensor parsing, error handling, format validation
  * All tests passing with Mox SNMP adapter mocking

Technical notes:
- Client.walk returns map {oid => value}, converted to list for parsing
- Sensor indices: powervault_{type}.{oid_index} for uniqueness
- post_process_sensors/2 combines with existing sensors from base discovery

Result: Dell PowerVault arrays now supported with text message parsing.
Gap: CRITICAL (no sensors) → RESOLVED. Parity: 0% → 85%.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 08:38:30 -06:00

267 lines
8.3 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")
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 = render_click(view, "show_add_token_modal")
assert html =~ "Create API Token"
end
test "cancels add token modal", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=api")
render_click(view, "show_add_token_modal")
html = render_click(view, "cancel_add_token")
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")
render_click(view, "show_add_token_modal")
html =
render_click(view, "create_api_token", %{
"token" => %{"name" => "My Token", "organization_id" => org.id}
})
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")
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")
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 notification tab with add device modal", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=notifications")
html = render_click(view, "show_add_device_modal")
assert html =~ "Mobile" or html =~ "Device"
html = render_click(view, "cancel_add_device")
assert html =~ "Mobile 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")
html = render_click(view, "check_password_breach", %{"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")
html = render_click(view, "check_password_breach", %{})
assert html =~ "Account Settings"
end
end
end