## Summary - Every device now automatically gets an ICMP ping check (`source_type: "auto_discovery"`) when created via `Monitoring.ensure_default_ping_check/1` - When a device IP changes, the ping check host config is synced automatically - Backfill migration inserts ICMP Ping checks for all existing devices that don't have one - Fixes checkbox alignment in the Add Service Check modal (verify SSL / follow redirects) ## Test plan - [x] 4 new tests for `ensure_default_ping_check/1` (create, idempotent, IP update, no-op) - [x] 3 new tests for device lifecycle (auto-create on device create, sync on IP change, no-op when IP unchanged) - [x] 13 existing tests updated to account for auto-created ping check - [x] Full test suite passes (`mix precommit`) - [ ] Verify backfill migration on staging - [ ] Create device in UI, confirm ping check appears in service checks list - [ ] Verify checkbox alignment in Add Service Check modal Reviewed-on: graham/towerops-web#68
540 lines
18 KiB
Elixir
540 lines
18 KiB
Elixir
defmodule ToweropsWeb.DeviceLive.ShowTest do
|
|
use ToweropsWeb.ConnCase, async: true
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Towerops.Snmp.Device
|
|
|
|
setup do
|
|
user = Towerops.AccountsFixtures.user_fixture(enable_totp: true)
|
|
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
|
|
|
device =
|
|
Towerops.DevicesFixtures.device_fixture(%{
|
|
organization_id: organization.id,
|
|
name: "Test Router",
|
|
ip_address: "192.168.1.1"
|
|
})
|
|
|
|
%{user: user, organization: organization, device: device}
|
|
end
|
|
|
|
describe "authenticated access" do
|
|
test "redirects to login when not authenticated", %{conn: conn, device: device} do
|
|
assert {:error, redirect} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
assert {:redirect, %{to: to}} = redirect
|
|
assert to =~ "/users/log-in"
|
|
end
|
|
end
|
|
|
|
describe "mount and display" do
|
|
test "displays device name and IP address", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
assert html =~ "Test Router"
|
|
assert html =~ "192.168.1.1"
|
|
end
|
|
|
|
test "redirects to add tab=overview when no tab param", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
|
|
# Without a tab param, the LiveView does push_patch to add ?tab=overview
|
|
assert {:error, {:live_redirect, %{to: to}}} = live(conn, ~p"/devices/#{device.id}")
|
|
assert to =~ "tab=overview"
|
|
end
|
|
|
|
test "sets page title to device name", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
assert page_title(view) =~ "Test Router"
|
|
end
|
|
end
|
|
|
|
describe "checks tab" do
|
|
test "displays checks tab link", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
assert html =~ "Checks"
|
|
end
|
|
|
|
test "loads checks tab showing auto-created ping check", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=checks")
|
|
|
|
assert html =~ "ICMP Ping"
|
|
assert html =~ "Add Check"
|
|
end
|
|
end
|
|
|
|
describe "tab navigation" do
|
|
test "renders overview tab with device information", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
assert html =~ "Device Information"
|
|
end
|
|
|
|
test "switches to neighbors tab via patch", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
# Clicking the Neighbors tab link triggers a patch (not a redirect)
|
|
html = view |> element("a", "Neighbors") |> render_click()
|
|
|
|
assert html =~ "Neighbors"
|
|
end
|
|
|
|
test "renders logs tab directly", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=logs")
|
|
|
|
assert html =~ "Test Router"
|
|
end
|
|
end
|
|
|
|
describe "organization access control" do
|
|
test "denies access to device from another organization", %{conn: conn, device: device} do
|
|
other_user = Towerops.AccountsFixtures.user_fixture(enable_totp: true)
|
|
|
|
{:ok, _other_org} =
|
|
Towerops.Organizations.create_organization(%{name: "Other Org"}, other_user.id)
|
|
|
|
conn = log_in_user(conn, other_user)
|
|
|
|
assert {:error, {:live_redirect, %{to: "/devices", flash: flash}}} =
|
|
live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
assert flash["error"] =~ "You don't have access to this device"
|
|
end
|
|
|
|
test "redirects with not found for nonexistent device", %{conn: conn, user: user} do
|
|
conn = log_in_user(conn, user)
|
|
fake_id = Ecto.UUID.generate()
|
|
|
|
assert {:error, {:live_redirect, %{to: "/devices", flash: flash}}} =
|
|
live(conn, ~p"/devices/#{fake_id}?tab=overview")
|
|
|
|
assert flash["error"] =~ "Device not found"
|
|
end
|
|
end
|
|
|
|
describe "tab rendering" do
|
|
test "renders interfaces tab", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=interfaces")
|
|
|
|
assert html =~ "Test Router"
|
|
end
|
|
|
|
test "renders sensors tab", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=sensors")
|
|
|
|
assert html =~ "Test Router"
|
|
end
|
|
|
|
test "renders events tab", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=events")
|
|
|
|
assert html =~ "Test Router"
|
|
end
|
|
end
|
|
|
|
describe "handle_info events" do
|
|
test "handles device_status_changed", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:device_status_changed, device.id, :up, 10})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles discovery_completed", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:discovery_completed, device.id})
|
|
html = render(view)
|
|
assert html =~ "Discovery completed"
|
|
end
|
|
|
|
test "handles sensors_updated", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:sensors_updated, device.id})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles interfaces_updated", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:interfaces_updated, device.id})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles neighbors_updated", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:neighbors_updated, device.id})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles arp_updated", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:arp_updated, device.id})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles mac_updated", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:mac_updated, device.id})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles state_sensors_updated", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:state_sensors_updated, device.id})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles processors_updated", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:processors_updated, device.id})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles storage_updated", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:storage_updated, device.id})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles monitoring_check_updated", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:monitoring_check_updated, device.id})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles device_event", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:device_event, %{device_id: device.id, type: "test"}})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles refresh_data", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, :refresh_data)
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles agents_stale with unrelated agent", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:agents_stale, []})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles agent_connected with unrelated agent", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:agent_connected, Ecto.UUID.generate(), Ecto.UUID.generate()})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles agent_disconnected with unrelated agent", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:agent_disconnected, Ecto.UUID.generate(), Ecto.UUID.generate()})
|
|
assert render(view)
|
|
end
|
|
|
|
test "handles agent_heartbeat with unrelated agent", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
send(view.pid, {:agent_heartbeat, Ecto.UUID.generate(), Ecto.UUID.generate()})
|
|
assert render(view)
|
|
end
|
|
end
|
|
|
|
describe "wireless tab" do
|
|
setup %{device: device} do
|
|
alias Device, as: SnmpDevice
|
|
|
|
snmp_device =
|
|
%SnmpDevice{}
|
|
|> SnmpDevice.changeset(%{device_id: device.id, sys_name: "ap", sys_descr: "Access Point"})
|
|
|> Towerops.Repo.insert!()
|
|
|
|
# Create wireless clients with varying signal quality
|
|
client1 =
|
|
Towerops.SnmpFixtures.wireless_client_fixture(%{
|
|
snmp_device_id: snmp_device.id,
|
|
device_id: device.id,
|
|
mac_address: "AA:BB:CC:DD:EE:01",
|
|
ip_address: "192.168.1.101",
|
|
signal_strength: -65,
|
|
snr: 25,
|
|
distance: 100,
|
|
tx_rate: 54_000,
|
|
rx_rate: 48_000,
|
|
uptime_seconds: 3661
|
|
})
|
|
|
|
client2 =
|
|
Towerops.SnmpFixtures.wireless_client_fixture(%{
|
|
snmp_device_id: snmp_device.id,
|
|
device_id: device.id,
|
|
mac_address: "AA:BB:CC:DD:EE:02",
|
|
ip_address: "192.168.1.102",
|
|
signal_strength: -85,
|
|
snr: 10,
|
|
distance: 250
|
|
})
|
|
|
|
%{snmp_device: snmp_device, client1: client1, client2: client2}
|
|
end
|
|
|
|
test "displays wireless tab link with client count badge", %{
|
|
conn: conn,
|
|
user: user,
|
|
device: device
|
|
} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
assert html =~ "Wireless"
|
|
assert html =~ "2"
|
|
end
|
|
|
|
test "renders wireless tab with client table", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=wireless")
|
|
|
|
# Check for table headers
|
|
assert html =~ "MAC Address"
|
|
assert html =~ "IP Address"
|
|
assert html =~ "Signal"
|
|
assert html =~ "SNR"
|
|
|
|
# Check for client data
|
|
assert html =~ "AA:BB:CC:DD:EE:01"
|
|
assert html =~ "192.168.1.101"
|
|
assert html =~ "AA:BB:CC:DD:EE:02"
|
|
assert html =~ "192.168.1.102"
|
|
end
|
|
|
|
test "displays signal strength badges with correct values", %{
|
|
conn: conn,
|
|
user: user,
|
|
device: device
|
|
} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=wireless")
|
|
|
|
assert html =~ "-65 dBm"
|
|
assert html =~ "-85 dBm"
|
|
end
|
|
|
|
test "displays SNR badges with correct values", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=wireless")
|
|
|
|
assert html =~ "25 dB"
|
|
assert html =~ "10 dB"
|
|
end
|
|
|
|
test "displays formatted uptime", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=wireless")
|
|
|
|
# 3661 seconds = 1 hour, 1 minute, 1 second
|
|
assert html =~ "1 hour"
|
|
end
|
|
|
|
test "displays formatted TX/RX rates", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=wireless")
|
|
|
|
# 54000 Kbps = 54 Mbps
|
|
assert html =~ "54"
|
|
# 48000 Kbps = 48 Mbps
|
|
assert html =~ "48"
|
|
end
|
|
|
|
test "handles wireless_clients_updated PubSub message", %{
|
|
conn: conn,
|
|
user: user,
|
|
device: device
|
|
} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=wireless")
|
|
|
|
# Send PubSub update
|
|
send(view.pid, {:wireless_clients_updated, device.id, 2})
|
|
|
|
# Re-render and verify still works
|
|
html = render(view)
|
|
assert html =~ "AA:BB:CC:DD:EE:01"
|
|
end
|
|
|
|
test "shows empty state when no wireless clients", %{conn: conn, user: user, organization: organization} do
|
|
alias Device, as: SnmpDevice
|
|
# Create a new device without wireless clients
|
|
device_without_clients =
|
|
Towerops.DevicesFixtures.device_fixture(%{
|
|
organization_id: organization.id,
|
|
name: "AP Without Clients",
|
|
ip_address: "192.168.1.2"
|
|
})
|
|
|
|
# Create snmp_device so the wireless tab appears
|
|
%SnmpDevice{}
|
|
|> SnmpDevice.changeset(%{
|
|
device_id: device_without_clients.id,
|
|
sys_name: "ap",
|
|
sys_descr: "AP"
|
|
})
|
|
|> Towerops.Repo.insert!()
|
|
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device_without_clients.id}?tab=wireless")
|
|
|
|
assert html =~ "No wireless clients"
|
|
end
|
|
|
|
test "switches to wireless tab via patch", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
|
|
|
|
html = view |> element("a", "Wireless") |> render_click()
|
|
|
|
assert html =~ "MAC Address"
|
|
assert html =~ "AA:BB:CC:DD:EE:01"
|
|
end
|
|
end
|
|
|
|
describe "ports tab capacity" do
|
|
setup %{device: device} do
|
|
alias Device, as: SnmpDevice
|
|
alias Towerops.Snmp.Interface
|
|
alias Towerops.Snmp.InterfaceStat
|
|
|
|
snmp_device =
|
|
%SnmpDevice{}
|
|
|> SnmpDevice.changeset(%{device_id: device.id, sys_name: "radio", sys_descr: "PTP"})
|
|
|> Towerops.Repo.insert!()
|
|
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0",
|
|
if_speed: 1_000_000_000,
|
|
if_type: 6,
|
|
configured_capacity_bps: 300_000_000,
|
|
capacity_source: "manual"
|
|
})
|
|
|> Towerops.Repo.insert!()
|
|
|
|
# Insert stats for utilization calculation
|
|
now = DateTime.utc_now()
|
|
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 0,
|
|
if_out_octets: 0,
|
|
checked_at: DateTime.add(now, -120, :second)
|
|
})
|
|
|> Towerops.Repo.insert!()
|
|
|
|
# ~150 Mbps = 50% of 300 Mbps capacity
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 0,
|
|
if_out_octets: 1_125_000_000,
|
|
checked_at: DateTime.add(now, -60, :second)
|
|
})
|
|
|> Towerops.Repo.insert!()
|
|
|
|
%{snmp_device: snmp_device, interface: interface}
|
|
end
|
|
|
|
test "displays capacity column on ports tab", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=ports")
|
|
|
|
assert html =~ "Capacity"
|
|
assert html =~ "300.0 Mbps"
|
|
end
|
|
|
|
test "displays utilization bar on ports tab", %{conn: conn, user: user, device: device} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=ports")
|
|
|
|
assert html =~ "Utilization"
|
|
end
|
|
|
|
test "set capacity via event handler", %{
|
|
conn: conn,
|
|
user: user,
|
|
device: device,
|
|
interface: interface
|
|
} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=ports")
|
|
|
|
html =
|
|
render_click(view, "set_capacity", %{
|
|
"interface_id" => interface.id,
|
|
"capacity_mbps" => "500"
|
|
})
|
|
|
|
assert html =~ "500.0 Mbps"
|
|
end
|
|
|
|
test "clear capacity via event handler", %{
|
|
conn: conn,
|
|
user: user,
|
|
device: device,
|
|
interface: interface
|
|
} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=ports")
|
|
|
|
html = render_click(view, "clear_capacity", %{"interface_id" => interface.id})
|
|
|
|
# After clearing, the capacity badge should not show 300 Mbps anymore
|
|
refute html =~ "300.0 Mbps"
|
|
end
|
|
end
|
|
end
|