- Make device name field optional when SNMP is enabled (ICMP Only mode still requires name) - Add database migration to make name column nullable - Add conditional validation: name required only when snmp_enabled is false - Update SNMP discovery to populate device name from sysName when empty - Add helper text explaining SNMP name population behavior in form - Add comprehensive tests for device name SNMP population - Update test mocks to account for NetSnmp profile sensor discovery
574 lines
17 KiB
Elixir
574 lines
17 KiB
Elixir
defmodule ToweropsWeb.DeviceLiveTest do
|
|
use ToweropsWeb.ConnCase
|
|
|
|
import Mox
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Towerops.Snmp.Device
|
|
alias Towerops.Snmp.SnmpMock
|
|
|
|
setup :register_and_log_in_user
|
|
setup :verify_on_exit!
|
|
|
|
# Set up SNMP mock globally to allow background tasks
|
|
setup :set_mox_global
|
|
|
|
setup do
|
|
# Stub SNMP mock globally to allow background task calls
|
|
Mox.stub(SnmpMock, :get, fn _target, _oid, _opts -> {:error, :timeout} end)
|
|
Mox.stub(SnmpMock, :walk, fn _target, _oid, _opts -> {:error, :timeout} end)
|
|
Mox.stub(SnmpMock, :get_bulk, fn _target, _oid, _opts -> {:error, :timeout} end)
|
|
|
|
:ok
|
|
end
|
|
|
|
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" do
|
|
test "lists all devices", %{conn: conn, organization: organization, site: site} do
|
|
{:ok, device} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Router 1",
|
|
ip_address: "192.168.1.1",
|
|
description: "Main router",
|
|
site_id: site.id
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices")
|
|
|
|
assert html =~ "Device"
|
|
assert html =~ device.name
|
|
assert html =~ "192.168.1.1"
|
|
end
|
|
|
|
test "displays empty state when no device", %{conn: conn, organization: organization} do
|
|
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices")
|
|
|
|
assert html =~ "No devices"
|
|
end
|
|
|
|
test "has link to add new device", %{conn: conn, organization: organization} do
|
|
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices")
|
|
|
|
assert html =~ "New Device"
|
|
end
|
|
|
|
test "requires authentication", %{organization: organization} do
|
|
conn = build_conn()
|
|
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/devices")
|
|
|
|
assert {:redirect, %{to: path}} = redirect
|
|
assert path == ~p"/users/log-in"
|
|
end
|
|
end
|
|
|
|
describe "Show" do
|
|
setup %{site: site} do
|
|
{:ok, device} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Router 1",
|
|
ip_address: "192.168.1.1",
|
|
description: "Main router",
|
|
site_id: site.id,
|
|
monitoring_enabled: true,
|
|
check_interval_seconds: 300
|
|
})
|
|
|
|
%{device: device}
|
|
end
|
|
|
|
test "displays device details", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
device: device
|
|
} do
|
|
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
|
|
|
assert html =~ device.name
|
|
assert html =~ "192.168.1.1"
|
|
assert html =~ "Device Information"
|
|
end
|
|
|
|
test "displays device with tab parameter", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
device: device
|
|
} do
|
|
{:ok, _view, html} =
|
|
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}?tab=interfaces")
|
|
|
|
assert html =~ device.name
|
|
assert html =~ "192.168.1.1"
|
|
end
|
|
|
|
test "shows device status when monitoring enabled", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
device: device
|
|
} do
|
|
# Create a monitoring check
|
|
{:ok, _check} =
|
|
Towerops.Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 25,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
|
|
|
# Should display monitoring information
|
|
assert html =~ device.name
|
|
assert html =~ "192.168.1.1"
|
|
end
|
|
|
|
test "shows SNMP device information when available", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
device: device
|
|
} do
|
|
# Create SNMP device record using Repo
|
|
_device =
|
|
%Device{}
|
|
|> Device.changeset(%{
|
|
device_id: device.id,
|
|
sys_descr: "Test Device",
|
|
sys_name: "test-device",
|
|
manufacturer: "Test Manufacturer",
|
|
model: "Model 1"
|
|
})
|
|
|> Towerops.Repo.insert!()
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
|
|
|
# Verify the page loads successfully with equipment data
|
|
assert html =~ device.name
|
|
assert html =~ "192.168.1.1"
|
|
end
|
|
|
|
test "displays recent events", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
device: device
|
|
} do
|
|
# Create an event with all required fields (use valid event_type)
|
|
{:ok, _event} =
|
|
Towerops.Devices.create_event(%{
|
|
device_id: device.id,
|
|
event_type: "device_discovered",
|
|
severity: "info",
|
|
message: "Device was discovered",
|
|
details: %{manufacturer: "Test", model: "Model 1"},
|
|
occurred_at: DateTime.utc_now()
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
|
|
|
# Verify page loads with events section
|
|
assert html =~ device.name
|
|
end
|
|
|
|
test "handles missing device gracefully", %{conn: conn, organization: organization} do
|
|
fake_id = Ecto.UUID.generate()
|
|
|
|
assert_raise Ecto.NoResultsError, fn ->
|
|
live(conn, ~p"/orgs/#{organization.slug}/devices/#{fake_id}")
|
|
end
|
|
end
|
|
|
|
test "deletes device", %{conn: conn, organization: organization, device: device} do
|
|
{:ok, view, _html} =
|
|
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
|
|
|
{:ok, _, html} =
|
|
view
|
|
|> element("button", "Delete Device")
|
|
|> render_click()
|
|
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices")
|
|
|
|
assert html =~ "Device deleted successfully"
|
|
end
|
|
|
|
test "requires authentication", %{organization: organization, device: device} do
|
|
conn = build_conn()
|
|
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
|
|
|
assert {:redirect, %{to: path}} = redirect
|
|
assert path == ~p"/users/log-in"
|
|
end
|
|
end
|
|
|
|
describe "New" do
|
|
test "renders new device form", %{conn: conn, organization: organization} do
|
|
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
assert html =~ "New Device"
|
|
assert html =~ "Add new device to monitor"
|
|
end
|
|
|
|
test "creates new device and stays on add page with success message", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
# Default is SNMP & ICMP mode, so snmp fields should be visible
|
|
html =
|
|
view
|
|
|> form("#device-form",
|
|
device: %{
|
|
name: "New Router",
|
|
ip_address: "192.168.1.100",
|
|
site_id: site.id,
|
|
description: "Test router",
|
|
monitoring_enabled: true,
|
|
check_interval_seconds: 300,
|
|
snmp_version: "2c",
|
|
snmp_community: "public",
|
|
snmp_port: 161
|
|
}
|
|
)
|
|
|> render_submit()
|
|
|
|
# Verify we stayed on the add page (no redirect)
|
|
assert html =~ "New Device"
|
|
assert html =~ "Add new device to monitor"
|
|
|
|
# Verify success message is shown
|
|
assert html =~ "Device created successfully"
|
|
|
|
# Verify device was created
|
|
device_list = Towerops.Devices.list_organization_devices(organization.id)
|
|
refute Enum.empty?(device_list)
|
|
|
|
new_device = Enum.find(device_list, &(&1.name == "New Router"))
|
|
assert new_device
|
|
assert new_device.ip_address == "192.168.1.100"
|
|
assert new_device.snmp_enabled == true
|
|
|
|
# Verify form is cleared for next device
|
|
assert html =~ "name=\"device[name]\""
|
|
refute html =~ "value=\"New Router\""
|
|
end
|
|
|
|
test "allows adding multiple devices in a row", %{conn: conn, organization: organization, site: site} do
|
|
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
# Add first device
|
|
view
|
|
|> form("#device-form",
|
|
device: %{
|
|
name: "Router 1",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
snmp_community: "public"
|
|
}
|
|
)
|
|
|> render_submit()
|
|
|
|
# Add second device (still on same view)
|
|
html =
|
|
view
|
|
|> form("#device-form",
|
|
device: %{
|
|
name: "Router 2",
|
|
ip_address: "192.168.1.2",
|
|
site_id: site.id,
|
|
snmp_community: "public"
|
|
}
|
|
)
|
|
|> render_submit()
|
|
|
|
# Verify both devices were created
|
|
device_list = Towerops.Devices.list_organization_devices(organization.id)
|
|
assert length(device_list) == 2
|
|
assert Enum.any?(device_list, &(&1.name == "Router 1"))
|
|
assert Enum.any?(device_list, &(&1.name == "Router 2"))
|
|
|
|
# Verify success message for second device
|
|
assert html =~ "Device created successfully"
|
|
end
|
|
|
|
test "validates required fields", %{conn: conn, organization: organization} do
|
|
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
html =
|
|
view
|
|
|> form("#device-form", device: %{name: "", ip_address: ""})
|
|
|> render_submit()
|
|
|
|
assert html =~ "can't be blank"
|
|
end
|
|
|
|
test "validates IP address format", %{conn: conn, organization: organization, site: site} do
|
|
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
html =
|
|
view
|
|
|> form("#device-form",
|
|
device: %{
|
|
name: "Router",
|
|
ip_address: "invalid-ip",
|
|
site_id: site.id
|
|
}
|
|
)
|
|
|> render_submit()
|
|
|
|
assert html =~ "must be a valid IPv4 or IPv6 address"
|
|
end
|
|
|
|
test "pre-selects site from query param", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, _view, html} =
|
|
live(conn, ~p"/orgs/#{organization.slug}/devices/new?site_id=#{site.id}")
|
|
|
|
assert html =~ "New Device"
|
|
end
|
|
|
|
test "requires authentication", %{organization: organization} do
|
|
conn = build_conn()
|
|
{:error, redirect} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
assert {:redirect, %{to: path}} = redirect
|
|
assert path == ~p"/users/log-in"
|
|
end
|
|
|
|
test "allows creating device without name when SNMP enabled", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
html =
|
|
view
|
|
|> form("#device-form",
|
|
device: %{
|
|
name: "",
|
|
ip_address: "192.168.1.150",
|
|
site_id: site.id,
|
|
snmp_community: "public"
|
|
}
|
|
)
|
|
|> render_submit()
|
|
|
|
# Should succeed without validation error
|
|
assert html =~ "Device created successfully"
|
|
refute html =~ "can't be blank"
|
|
|
|
# Verify device was created with empty name
|
|
device_list = Towerops.Devices.list_organization_devices(organization.id)
|
|
new_device = Enum.find(device_list, &(&1.ip_address == "192.168.1.150"))
|
|
assert new_device
|
|
assert new_device.name == nil or new_device.name == ""
|
|
end
|
|
|
|
test "shows helper text about SNMP name population when name is empty", %{
|
|
conn: conn,
|
|
organization: organization
|
|
} do
|
|
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
# Should show helper text about name being populated from SNMP
|
|
assert html =~ "Leave blank to use SNMP device name" or
|
|
html =~ "populated from SNMP" or
|
|
html =~ "sysName"
|
|
end
|
|
|
|
test "requires name when ICMP Only mode (no SNMP)", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
# Switch to ICMP Only mode
|
|
view
|
|
|> element("button", "ICMP Only")
|
|
|> render_click()
|
|
|
|
html =
|
|
view
|
|
|> form("#device-form",
|
|
device: %{
|
|
name: "",
|
|
ip_address: "192.168.1.151",
|
|
site_id: site.id
|
|
}
|
|
)
|
|
|> render_submit()
|
|
|
|
# Should show validation error for empty name in ICMP Only mode
|
|
assert html =~ "can't be blank" or html =~ "required"
|
|
end
|
|
|
|
test "defaults to SNMP & ICMP monitoring mode", %{conn: conn, organization: organization} do
|
|
{:ok, _view, html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
assert html =~ "SNMP & ICMP"
|
|
assert html =~ "ICMP Only"
|
|
assert html =~ "SNMP Version"
|
|
assert html =~ "Community String"
|
|
end
|
|
|
|
test "switching to ICMP Only mode hides SNMP configuration", %{
|
|
conn: conn,
|
|
organization: organization
|
|
} do
|
|
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
html =
|
|
view
|
|
|> element("button", "ICMP Only")
|
|
|> render_click()
|
|
|
|
refute html =~ "SNMP Version"
|
|
refute html =~ "Community String"
|
|
end
|
|
|
|
test "switching back to SNMP & ICMP mode shows SNMP configuration", %{
|
|
conn: conn,
|
|
organization: organization
|
|
} do
|
|
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
# First switch to ICMP Only
|
|
view
|
|
|> element("button", "ICMP Only")
|
|
|> render_click()
|
|
|
|
# Then switch back to SNMP & ICMP
|
|
html =
|
|
view
|
|
|> element("button", "SNMP & ICMP")
|
|
|> render_click()
|
|
|
|
assert html =~ "SNMP Version"
|
|
assert html =~ "Community String"
|
|
end
|
|
|
|
test "creates device with ICMP only mode (snmp_enabled false)", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
# Switch to ICMP Only mode
|
|
view
|
|
|> element("button", "ICMP Only")
|
|
|> render_click()
|
|
|
|
view
|
|
|> form("#device-form",
|
|
device: %{
|
|
name: "ICMP Only Device",
|
|
ip_address: "192.168.1.200",
|
|
site_id: site.id,
|
|
monitoring_enabled: true
|
|
}
|
|
)
|
|
|> render_submit()
|
|
|
|
# Verify device was created with snmp_enabled = false
|
|
device_list = Towerops.Devices.list_organization_devices(organization.id)
|
|
new_device = Enum.find(device_list, &(&1.name == "ICMP Only Device"))
|
|
|
|
assert new_device
|
|
assert new_device.snmp_enabled == false
|
|
end
|
|
|
|
test "creates device with SNMP & ICMP mode (snmp_enabled true)", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, view, _html} = live(conn, ~p"/orgs/#{organization.slug}/devices/new")
|
|
|
|
# SNMP & ICMP is the default, so no need to click tab
|
|
|
|
view
|
|
|> form("#device-form",
|
|
device: %{
|
|
name: "SNMP Device",
|
|
ip_address: "192.168.1.201",
|
|
site_id: site.id,
|
|
monitoring_enabled: true,
|
|
snmp_community: "public"
|
|
}
|
|
)
|
|
|> render_submit()
|
|
|
|
# Verify device was created with snmp_enabled = true
|
|
device_list = Towerops.Devices.list_organization_devices(organization.id)
|
|
new_device = Enum.find(device_list, &(&1.name == "SNMP Device"))
|
|
|
|
assert new_device
|
|
assert new_device.snmp_enabled == true
|
|
end
|
|
end
|
|
|
|
describe "Edit" do
|
|
setup %{site: site} do
|
|
{:ok, device} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Router 1",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id
|
|
})
|
|
|
|
%{device: device}
|
|
end
|
|
|
|
test "renders edit form", %{conn: conn, organization: organization, device: device} do
|
|
{:ok, _view, html} =
|
|
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
|
|
|
assert html =~ "Edit Device"
|
|
assert html =~ "Update device details"
|
|
end
|
|
|
|
test "updates device", %{conn: conn, organization: organization, device: device} do
|
|
{:ok, view, _html} =
|
|
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
|
|
|
{:ok, _, html} =
|
|
view
|
|
|> form("#device-form",
|
|
device: %{
|
|
name: "Updated Router",
|
|
ip_address: "192.168.1.2"
|
|
}
|
|
)
|
|
|> render_submit()
|
|
|> follow_redirect(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}")
|
|
|
|
assert html =~ "Device updated successfully"
|
|
assert html =~ "Updated Router"
|
|
end
|
|
|
|
test "requires authentication", %{organization: organization, device: device} do
|
|
conn = build_conn()
|
|
|
|
{:error, redirect} =
|
|
live(conn, ~p"/orgs/#{organization.slug}/devices/#{device.id}/edit")
|
|
|
|
assert {:redirect, %{to: path}} = redirect
|
|
assert path == ~p"/users/log-in"
|
|
end
|
|
end
|
|
end
|