towerops/test/towerops_web/live/device_live/form_test.exs
2026-06-15 11:15:46 -05:00

434 lines
13 KiB
Elixir

defmodule ToweropsWeb.DeviceLive.FormTest do
use ToweropsWeb.ConnCase, async: true
import Phoenix.LiveViewTest
alias Towerops.Snmp.Device
alias Towerops.Snmp.Device, as: SnmpDevice
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 "edit page rendering" do
test "renders edit page when device has no SNMP discovery data (bug fix for nil boolean)", %{
conn: conn,
site: site,
organization: organization
} do
# Regression test for production bug where visiting edit page for a device
# with no SNMP discovery data would crash with "expected boolean, got nil"
# because mikrotik_device?/1 could return nil instead of false.
# Create a device without any snmp_device association (before SNMP discovery)
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Test Device",
ip_address: "192.168.1.1",
site_id: site.id,
organization_id: organization.id,
snmp_enabled: true,
snmp_version: "2c",
snmp_port: 161,
monitoring_enabled: true,
check_interval_seconds: 300
})
# Ensure device has no snmp_device (this is the bug condition)
device_with_snmp = Towerops.Repo.preload(device, :snmp_device)
assert is_nil(device_with_snmp.snmp_device)
# Visit edit page - should not crash with "expected boolean, got nil" error
assert {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit")
# Page should render successfully
assert html =~ "Edit Device"
assert html =~ device.name
end
test "MikroTik section is hidden even for MikroTik devices (currently disabled)", %{
conn: conn,
site: site,
organization: organization
} do
# Create a device with MikroTik SNMP discovery data
{:ok, device} =
Towerops.Devices.create_device(%{
name: "MikroTik Router",
ip_address: "192.168.1.254",
site_id: site.id,
organization_id: organization.id,
snmp_enabled: true,
snmp_version: "2c",
snmp_port: 161,
monitoring_enabled: true,
check_interval_seconds: 300
})
# Create SNMP device with MikroTik manufacturer (using Towerops.Snmp.Device schema)
_snmp_device =
%SnmpDevice{}
|> SnmpDevice.changeset(%{
device_id: device.id,
manufacturer: "MikroTik",
sys_descr: "RouterOS 7.8",
sys_object_id: "1.3.6.1.4.1.14988.1",
sys_uptime: 0,
sys_name: "test-router"
})
|> Towerops.Repo.insert!()
# Visit edit page
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit")
# Page should render successfully
assert html =~ "Edit Device"
assert html =~ device.name
# MikroTik section is currently disabled in the template
refute html =~ "MikroTik API Configuration"
end
test "does not render MikroTik section when device is not MikroTik", %{
conn: conn,
site: site,
organization: organization
} do
# Create a device with non-MikroTik SNMP discovery data
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Cisco Switch",
ip_address: "192.168.1.253",
site_id: site.id,
organization_id: organization.id,
snmp_enabled: true,
snmp_version: "2c",
snmp_port: 161,
monitoring_enabled: true,
check_interval_seconds: 300
})
# Create SNMP device with Cisco manufacturer
_snmp_device =
%SnmpDevice{}
|> SnmpDevice.changeset(%{
device_id: device.id,
manufacturer: "Cisco Systems",
sys_descr: "Cisco IOS",
sys_object_id: "1.3.6.1.4.1.9",
sys_uptime: 0,
sys_name: "test-switch"
})
|> Towerops.Repo.insert!()
# Visit edit page
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit")
# Page should render successfully
assert html =~ "Edit Device"
assert html =~ device.name
# MikroTik section SHOULD NOT be visible for non-MikroTik devices
refute html =~ "MikroTik API Configuration"
end
end
describe "new device form" do
test "renders new device page", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/devices/new")
assert html =~ "New Device"
end
test "pre-fills from query params", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/devices/new?name=Prefilled&ip_address=10.0.0.1")
assert html =~ "Prefilled"
assert html =~ "10.0.0.1"
end
test "validates device form on change", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/devices/new")
html =
view
|> form("#device-form", device: %{name: "", ip_address: ""})
|> render_change()
assert html =~ "New Device"
end
test "submits new device form", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/devices/new")
result =
view
|> form("#device-form",
device: %{
name: "New Test Device",
ip_address: "10.0.0.100"
}
)
|> render_submit()
case result do
{:error, {:live_redirect, %{to: to}}} ->
assert to =~ "tab=overview"
html when is_binary(html) ->
# Form re-rendered (possibly validation), page still alive
assert html =~ ~r/New Device|New Test Device/
end
end
test "switches monitoring mode", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/devices/new")
html =
view
|> element("#monitoring-mode-icmp-only")
|> render_click()
assert html =~ "New Device"
end
end
describe "edit device form" do
setup %{site: site, organization: organization} do
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Edit Me",
ip_address: "192.168.1.50",
site_id: site.id,
organization_id: organization.id,
snmp_enabled: true
})
%{device: device}
end
test "renders edit page with current values", %{conn: conn, device: device} do
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit")
assert html =~ "Edit Device"
assert html =~ "Edit Me"
end
test "validates edit form on change", %{conn: conn, device: device} do
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/edit")
html =
view
|> form("#device-form", device: %{name: "Updated Name"})
|> render_change()
# The validation should not produce errors
refute html =~ "can't be blank"
refute html =~ "can't be blank"
# The form should accept the valid input and redirect to show page
assert {:error, {:live_redirect, %{to: to}}} =
view
|> form("#device-form", device: %{name: "Updated Name"})
|> render_submit()
assert to =~ ~r{/devices/[0-9a-f-]+}
end
test "saves edited device", %{conn: conn, device: device} do
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/edit")
assert {:error, {:live_redirect, %{to: to}}} =
view
|> form("#device-form", device: %{name: "Updated Router"})
|> render_submit()
assert to =~ device.id
end
test "deletes device", %{conn: conn, device: device} do
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/edit")
result = view |> element("#delete-device") |> render_click()
case result do
{:error, {:live_redirect, %{to: "/devices" <> _}}} ->
refute false
html when is_binary(html) ->
# If it redirects via push_navigate, the view dies
refute Process.alive?(view.pid)
end
end
test "trigger_discovery with SNMP disabled shows error", %{conn: conn, site: site, organization: organization} do
{:ok, device} =
Towerops.Devices.create_device(%{
name: "No SNMP",
ip_address: "192.168.1.99",
site_id: site.id,
organization_id: organization.id,
snmp_enabled: false
})
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/edit")
# The "Rediscover" button is hidden when monitoring_mode == "icmp_only" (i.e.
# when SNMP is disabled), so it cannot be clicked through the UI in this state.
# This test exercises the handler's defensive guard against being invoked anyway.
html = render_click(view, "trigger_discovery")
assert html =~ "SNMP is not enabled"
end
end
describe "device role selector" do
setup %{site: site, organization: organization} do
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Role Test Device",
ip_address: "192.168.1.60",
site_id: site.id,
organization_id: organization.id,
snmp_enabled: true
})
%{device: device}
end
test "new device form shows device type dropdown", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/devices/new")
assert html =~ "Device Type"
assert html =~ "Select device type"
assert html =~ "Router"
assert html =~ "Switch"
assert html =~ "Server"
assert html =~ "Access Point"
assert html =~ "Backhaul"
assert html =~ "Other"
end
test "edit device form shows device type dropdown", %{conn: conn, device: device} do
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit")
assert html =~ "Device Type"
assert html =~ "Select device type"
end
test "selecting a role saves the device role", %{conn: conn, device: device} do
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/edit")
assert {:error, {:live_redirect, %{to: to}}} =
view
|> form("#device-form", device: %{device_role: "router"})
|> render_submit()
assert to =~ device.id
updated_device = Towerops.Devices.get_device!(device.id)
assert updated_device.device_role == "router"
end
test "edit form shows current role as selected", %{
conn: conn,
site: site,
organization: organization
} do
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Router Device",
ip_address: "192.168.1.62",
site_id: site.id,
organization_id: organization.id,
snmp_enabled: true,
device_role: "router"
})
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit")
# The select should show the current role selected
assert html =~ "device_role"
assert html =~ "router"
end
end
describe "non_routable_ip?/1" do
# Access private function via Module.get_attribute or test via public interface
# Since these are private functions, we test through the validation behavior
test "RFC1918 10.0.0.0/8 range" do
assert non_routable?("10.0.0.1")
assert non_routable?("10.255.255.255")
assert non_routable?("10.100.50.25")
end
test "RFC1918 172.16.0.0/12 range" do
assert non_routable?("172.16.0.1")
assert non_routable?("172.31.255.255")
assert non_routable?("172.20.10.5")
refute non_routable?("172.15.0.1")
refute non_routable?("172.32.0.1")
end
test "RFC1918 192.168.0.0/16 range" do
assert non_routable?("192.168.0.1")
assert non_routable?("192.168.255.255")
assert non_routable?("192.168.1.100")
refute non_routable?("192.167.1.1")
refute non_routable?("192.169.1.1")
end
test "RFC6598 CGNAT 100.64.0.0/10 range" do
assert non_routable?("100.64.0.1")
assert non_routable?("100.127.255.255")
assert non_routable?("100.100.50.25")
refute non_routable?("100.63.255.255")
refute non_routable?("100.128.0.1")
end
test "public IPs are not flagged" do
refute non_routable?("8.8.8.8")
refute non_routable?("1.1.1.1")
refute non_routable?("203.0.113.1")
refute non_routable?("198.51.100.1")
end
test "invalid IPs are not flagged" do
refute non_routable?("invalid")
refute non_routable?("999.999.999.999")
refute non_routable?("")
end
test "IPv6 addresses are not flagged" do
refute non_routable?("::1")
refute non_routable?("fe80::1")
refute non_routable?("2001:db8::1")
end
end
# Helper to test the private function logic
# Duplicated here for testing since the actual function is private
defp non_routable?(ip_string) do
case ip_string |> String.to_charlist() |> :inet.parse_address() do
{:ok, {a, b, _c, _d}} -> non_routable_ipv4_range?(a, b)
_ -> false
end
end
defp non_routable_ipv4_range?(10, _b), do: true
defp non_routable_ipv4_range?(172, b) when b >= 16 and b <= 31, do: true
defp non_routable_ipv4_range?(192, 168), do: true
defp non_routable_ipv4_range?(100, b) when b >= 64 and b <= 127, do: true
defp non_routable_ipv4_range?(_a, _b), do: false
end