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>
240 lines
6.6 KiB
Elixir
240 lines
6.6 KiB
Elixir
defmodule ToweropsWeb.DeviceLive.IndexTest do
|
|
use ToweropsWeb.ConnCase, async: true
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Towerops.Devices
|
|
|
|
setup :register_and_log_in_user
|
|
|
|
setup %{user: user} do
|
|
{:ok, organization} =
|
|
Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
|
|
|
{:ok, site} =
|
|
Towerops.Sites.create_site(%{
|
|
name: "Test Site",
|
|
organization_id: organization.id
|
|
})
|
|
|
|
%{organization: organization, site: site}
|
|
end
|
|
|
|
describe "index page rendering" do
|
|
test "renders empty state when no devices", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/devices")
|
|
|
|
assert html =~ "No devices"
|
|
assert html =~ "New Device"
|
|
end
|
|
|
|
test "renders device list with devices", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Test Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/devices")
|
|
|
|
assert html =~ "Test Router"
|
|
assert html =~ "192.168.1.1"
|
|
end
|
|
|
|
test "renders site headers with device stats", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Router 1",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/devices")
|
|
|
|
assert html =~ "Test Site"
|
|
assert html =~ "1 device"
|
|
end
|
|
end
|
|
|
|
describe "real-time updates via PubSub" do
|
|
test "updates when a new device is created", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, view, html} = live(conn, ~p"/devices")
|
|
|
|
assert html =~ "No devices"
|
|
|
|
# Create a device (triggers PubSub broadcast)
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "New Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
# LiveView should receive PubSub event and re-render
|
|
html = render(view)
|
|
assert html =~ "New Router"
|
|
assert html =~ "192.168.1.1"
|
|
end
|
|
|
|
test "updates when a device is modified", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, device} =
|
|
Devices.create_device(%{
|
|
name: "Old Name",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, view, html} = live(conn, ~p"/devices")
|
|
|
|
assert html =~ "Old Name"
|
|
|
|
{:ok, _updated} = Devices.update_device(device, %{name: "New Name"})
|
|
|
|
html = render(view)
|
|
assert html =~ "New Name"
|
|
end
|
|
|
|
test "updates when a device status changes", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, device} =
|
|
Devices.create_device(%{
|
|
name: "Status Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
{:ok, _updated} = Devices.update_device_status(device, :up)
|
|
|
|
html = render(view)
|
|
assert html =~ "UP"
|
|
end
|
|
|
|
test "updates when a device is deleted", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, device} =
|
|
Devices.create_device(%{
|
|
name: "Delete Me",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, view, html} = live(conn, ~p"/devices")
|
|
|
|
assert html =~ "Delete Me"
|
|
|
|
{:ok, _deleted} = Devices.delete_device(device)
|
|
|
|
html = render(view)
|
|
refute html =~ "Delete Me"
|
|
assert html =~ "No devices"
|
|
end
|
|
|
|
test "updates device quota after changes", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Quota Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
html = render(view)
|
|
assert html =~ "1/10 devices"
|
|
end
|
|
end
|
|
|
|
describe "discovered tab" do
|
|
test "renders discovered devices tab", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/devices?tab=discovered")
|
|
|
|
assert html =~ "Discovered"
|
|
end
|
|
|
|
test "renders discovered tab with pagination", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/devices?tab=discovered&page=1")
|
|
|
|
assert html =~ "Discovered"
|
|
end
|
|
end
|
|
|
|
describe "device management events" do
|
|
test "toggles reorder mode", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
html = render_click(view, "toggle_reorder_mode")
|
|
assert html =~ "Reset Order"
|
|
end
|
|
|
|
test "resets device order", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
html = render_click(view, "reset_order")
|
|
assert html =~ "Order reset to alphabetical"
|
|
end
|
|
|
|
test "force_rediscover_all with no SNMP devices shows error", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id,
|
|
snmp_enabled: false
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
html = render_click(view, "force_rediscover_all")
|
|
assert html =~ "No SNMP-enabled devices found"
|
|
end
|
|
|
|
test "reorder_device with invalid device_id shows error", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
html =
|
|
render_click(view, "reorder_device", %{
|
|
"device_id" => Ecto.UUID.generate(),
|
|
"new_position" => "1"
|
|
})
|
|
|
|
assert html =~ "not found"
|
|
end
|
|
|
|
test "reorder_site with invalid site_id shows error", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
html =
|
|
render_click(view, "reorder_site", %{
|
|
"site_id" => Ecto.UUID.generate(),
|
|
"new_position" => "1"
|
|
})
|
|
|
|
assert html =~ "not found"
|
|
end
|
|
end
|
|
end
|