towerops/test/towerops_web/live/device_live/show_test.exs
Graham McIntire aa7fc830e5 test: refactor LiveView tests to use HTML-aware element/form helpers
Apply patterns from testingliveview.com — replace render_click/submit/change
event-name calls with element/form-targeted equivalents that validate the
actual DOM wiring, not just handler logic. Adds stable test selectors
(id="...") to interactive elements across 10 LiveView templates.

Surfaces (and fixes) several real issues that the bypass-style tests masked:
- Removes dead regenerate_token handler in agent_live/index.ex (no UI ever
  triggered it) and its corresponding test.
- Removes dead refresh_topology test in network_map_live_test (event was
  referenced only in the test — no handler, no button, no JS anywhere).
- Corrects "shows notification tab with add device modal" — modal lives on
  the security tab; original test mounted ?tab=notifications and only
  worked because direct event calls bypassed tab gating.
- Corrects form input shape mismatches in agent_live/index_test that the
  handler's defensive case clauses had been masking.
- Expands setup data for org/settings_live_test (snmp_community,
  default_agent_token_id, multi-org membership) so the gated buttons
  actually render.

11 direct render_click/submit/change calls remain — all intentional,
documented server-side guard tests (tampered POSTs, race conditions,
defensive idempotent handlers).

Full LiveView test suite: 1300 tests, 0 failures.
2026-04-29 11:14:20 -05:00

1018 lines
32 KiB
Elixir

defmodule ToweropsWeb.DeviceLive.ShowTest do
use ToweropsWeb.ConnCase, async: true
import Phoenix.LiveViewTest
alias Towerops.Snmp.Device
alias Towerops.Snmp.Interface
alias Towerops.Snmp.InterfaceStat
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
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 UI button", %{
conn: conn,
user: user,
device: device,
interface: interface
} do
# The "Set" button only appears when the interface has no configured_capacity yet.
# Clear it first so the button is visible, then click — the button submits the
# if_speed-derived value (1_000_000_000 / 1M = 1000 Mbps).
interface
|> Interface.changeset(%{configured_capacity_bps: nil, capacity_source: "if_speed"})
|> Towerops.Repo.update!()
conn = log_in_user(conn, user)
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=ports")
html =
view
|> element("#set-capacity-#{interface.id}")
|> render_click()
assert html =~ "1000.0 Mbps" or html =~ "1.0 Gbps"
end
test "clear capacity via UI button", %{
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 =
view
|> element("#clear-capacity-#{interface.id}")
|> render_click()
# After clearing, the capacity badge should not show 300 Mbps anymore
refute html =~ "300.0 Mbps"
end
end
describe "overall traffic chart" do
setup %{device: device} do
snmp_device =
%Device{}
|> Device.changeset(%{device_id: device.id, sys_name: "radio", sys_descr: "PTP"})
|> Towerops.Repo.insert!()
device =
device
|> Ecto.Changeset.change(device_role: "backhaul")
|> Towerops.Repo.update!()
interface =
%Interface{}
|> Interface.changeset(%{
snmp_device_id: snmp_device.id,
if_index: 1,
if_name: "eth0",
if_descr: "eth0",
if_speed: 1_000_000_000,
if_type: 6,
configured_capacity_bps: 500_000_000,
capacity_source: "manual"
})
|> Towerops.Repo.insert!()
now = DateTime.utc_now()
%InterfaceStat{}
|> InterfaceStat.changeset(%{
interface_id: interface.id,
if_in_octets: 1_000,
if_out_octets: 2_000,
checked_at: DateTime.add(now, -60, :second)
})
|> Towerops.Repo.insert!()
%InterfaceStat{}
|> InterfaceStat.changeset(%{
interface_id: interface.id,
if_in_octets: 10_000,
if_out_octets: 14_000,
checked_at: now
})
|> Towerops.Repo.insert!()
%{device: device}
end
test "renders overall backhaul traffic and capacity with consistent polarity", %{
conn: conn,
user: user,
device: device
} do
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=overview")
datasets = chart_datasets_from_html(html, "traffic-chart")
assert dataset_y_values(datasets, "Outbound") == [-1600.0]
assert dataset_y_values(datasets, "Inbound") == [1200.0]
assert dataset_y_values(datasets, "Capacity (In)") == [500_000_000.0]
assert dataset_y_values(datasets, "Capacity (Out)") == [-500_000_000.0]
assert dataset_fill(datasets, "Capacity (In)") == false
assert dataset_fill(datasets, "Capacity (Out)") == false
end
end
defp chart_datasets_from_html(html, chart_id) do
pattern = ~r/id="#{chart_id}"[^>]*data-chart="([^"]+)"/
[_, encoded_json] = Regex.run(pattern, html)
encoded_json
|> String.replace(""", "\"")
|> Jason.decode!()
|> Map.fetch!("datasets")
end
defp dataset_y_values(datasets, label) do
datasets
|> Enum.find(&(Map.get(&1, "label") == label))
|> Map.fetch!("data")
|> Enum.map(&Map.fetch!(&1, "y"))
end
defp dataset_fill(datasets, label) do
datasets
|> Enum.find(&(Map.get(&1, "label") == label))
|> Map.get("fill")
end
describe "transceivers tab" do
setup %{device: device} do
alias Device, as: SnmpDevice
alias Towerops.Snmp.Transceiver
snmp_device =
%SnmpDevice{}
|> SnmpDevice.changeset(%{device_id: device.id, sys_name: "switch", sys_descr: "Switch"})
|> Towerops.Repo.insert!()
transceiver1 =
%Transceiver{}
|> Transceiver.changeset(%{
snmp_device_id: snmp_device.id,
port_index: "1001",
transceiver_type: "SFP+",
vendor_name: "Finisar",
vendor_part_number: "FTLX8571D3BCL",
vendor_serial_number: "ABC123456",
wavelength_nm: 850,
supports_dom: true
})
|> Towerops.Repo.insert!()
transceiver2 =
%Transceiver{}
|> Transceiver.changeset(%{
snmp_device_id: snmp_device.id,
port_index: "1002",
transceiver_type: "QSFP28",
vendor_name: "Intel",
vendor_part_number: "E810-XXVDA4",
supports_dom: false
})
|> Towerops.Repo.insert!()
%{snmp_device: snmp_device, transceiver1: transceiver1, transceiver2: transceiver2}
end
test "displays transceivers 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 =~ "Transceivers"
end
test "renders transceivers tab with table", %{conn: conn, user: user, device: device} do
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=transceivers")
# Check for table headers
assert html =~ "Port"
assert html =~ "Type"
assert html =~ "Vendor"
assert html =~ "Part Number"
assert html =~ "Serial Number"
# Check for transceiver data
assert html =~ "SFP+"
assert html =~ "QSFP28"
assert html =~ "Finisar"
assert html =~ "Intel"
assert html =~ "FTLX8571D3BCL"
assert html =~ "ABC123456"
end
test "displays DOM badge for transceivers with supports_dom=true", %{
conn: conn,
user: user,
device: device
} do
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=transceivers")
assert html =~ "DOM"
end
test "shows empty state when no transceivers", %{conn: conn, user: user, organization: organization} do
alias Device, as: SnmpDevice
device_without_transceivers =
Towerops.DevicesFixtures.device_fixture(%{
organization_id: organization.id,
name: "Switch Without Transceivers",
ip_address: "192.168.1.3"
})
%SnmpDevice{}
|> SnmpDevice.changeset(%{
device_id: device_without_transceivers.id,
sys_name: "switch",
sys_descr: "Switch"
})
|> Towerops.Repo.insert!()
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/devices/#{device_without_transceivers.id}?tab=transceivers")
assert html =~ "No transceivers found"
end
test "switches to transceivers 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", "Transceivers") |> render_click()
assert html =~ "Port"
assert html =~ "SFP+"
end
end
describe "printer supplies tab" do
setup %{device: device} do
alias Device, as: SnmpDevice
alias Towerops.Snmp.PrinterSupply
snmp_device =
%SnmpDevice{}
|> SnmpDevice.changeset(%{device_id: device.id, sys_name: "printer", sys_descr: "Printer"})
|> Towerops.Repo.insert!()
supply1 =
%PrinterSupply{}
|> PrinterSupply.changeset(%{
snmp_device_id: snmp_device.id,
supply_index: "1",
supply_type: "toner",
supply_description: "Black Toner Cartridge",
supply_unit: "percent",
max_capacity: 100,
current_level: 45,
color_name: "black",
part_number: "TN-450"
})
|> Towerops.Repo.insert!()
supply2 =
%PrinterSupply{}
|> PrinterSupply.changeset(%{
snmp_device_id: snmp_device.id,
supply_index: "2",
supply_type: "drum",
supply_description: "Drum Unit",
supply_unit: "percent",
max_capacity: 100,
current_level: 15,
part_number: "DR-420"
})
|> Towerops.Repo.insert!()
%{snmp_device: snmp_device, supply1: supply1, supply2: supply2}
end
test "displays printer supplies 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 =~ "Printer Supplies"
end
test "renders printer supplies tab with table", %{conn: conn, user: user, device: device} do
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=printer_supplies")
# Check for table headers
assert html =~ "Type"
assert html =~ "Description"
assert html =~ "Level"
assert html =~ "Part Number"
# Check for supply data
assert html =~ "toner"
assert html =~ "drum"
assert html =~ "Black Toner Cartridge"
assert html =~ "Drum Unit"
assert html =~ "TN-450"
assert html =~ "DR-420"
end
test "displays supply level percentage bars", %{conn: conn, user: user, device: device} do
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=printer_supplies")
assert html =~ "45%"
assert html =~ "15%"
end
test "shows low supply warning for supplies below 20%", %{
conn: conn,
user: user,
device: device
} do
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=printer_supplies")
# Supply at 15% should have warning styling
assert html =~ "15%"
end
test "shows empty state when no printer supplies", %{
conn: conn,
user: user,
organization: organization
} do
alias Device, as: SnmpDevice
device_without_supplies =
Towerops.DevicesFixtures.device_fixture(%{
organization_id: organization.id,
name: "Device Without Supplies",
ip_address: "192.168.1.4"
})
%SnmpDevice{}
|> SnmpDevice.changeset(%{
device_id: device_without_supplies.id,
sys_name: "device",
sys_descr: "Device"
})
|> Towerops.Repo.insert!()
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/devices/#{device_without_supplies.id}?tab=printer_supplies")
assert html =~ "No printer supplies found"
end
test "switches to printer supplies 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", "Printer Supplies") |> render_click()
assert html =~ "Black Toner Cartridge"
assert html =~ "45%"
end
end
describe "hardware inventory tab" do
setup %{device: device} do
alias Device, as: SnmpDevice
alias Towerops.Snmp.EntityPhysical
snmp_device =
%SnmpDevice{}
|> SnmpDevice.changeset(%{device_id: device.id, sys_name: "switch", sys_descr: "Switch"})
|> Towerops.Repo.insert!()
# Create chassis (root level)
chassis =
%EntityPhysical{}
|> EntityPhysical.changeset(%{
snmp_device_id: snmp_device.id,
entity_index: "1",
entity_class: "chassis",
name: "Chassis 1",
description: "Main Chassis",
serial_number: "SN123456",
model: "CCR1009",
manufacturer: "MikroTik"
})
|> Towerops.Repo.insert!()
# Create power supply (child of chassis)
power_supply =
%EntityPhysical{}
|> EntityPhysical.changeset(%{
snmp_device_id: snmp_device.id,
entity_index: "2",
parent_index: "1",
parent_id: chassis.id,
entity_class: "powerSupply",
name: "Power Supply 1",
description: "Main Power Supply",
serial_number: "PS789012"
})
|> Towerops.Repo.insert!()
# Create module (child of chassis)
module =
%EntityPhysical{}
|> EntityPhysical.changeset(%{
snmp_device_id: snmp_device.id,
entity_index: "3",
parent_index: "1",
parent_id: chassis.id,
entity_class: "module",
name: "SFP Module 1",
description: "10G SFP+ Module"
})
|> Towerops.Repo.insert!()
%{snmp_device: snmp_device, chassis: chassis, power_supply: power_supply, module: module}
end
test "displays hardware inventory 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 =~ "Hardware"
end
test "renders hardware inventory tab with table", %{conn: conn, user: user, device: device} do
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=hardware")
# Check for table headers
assert html =~ "Class"
assert html =~ "Name"
assert html =~ "Description"
assert html =~ "Serial Number"
assert html =~ "Model"
# Check for entity data
assert html =~ "chassis"
assert html =~ "powerSupply"
assert html =~ "module"
assert html =~ "Chassis 1"
assert html =~ "Power Supply 1"
assert html =~ "SN123456"
assert html =~ "CCR1009"
assert html =~ "MikroTik"
end
test "displays hierarchical structure with indentation", %{
conn: conn,
user: user,
device: device
} do
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}?tab=hardware")
# Power supply and module should appear after chassis
assert html =~ "Main Chassis"
assert html =~ "Main Power Supply"
assert html =~ "SFP Module 1"
end
test "shows empty state when no hardware inventory", %{
conn: conn,
user: user,
organization: organization
} do
alias Device, as: SnmpDevice
device_without_hardware =
Towerops.DevicesFixtures.device_fixture(%{
organization_id: organization.id,
name: "Device Without Hardware",
ip_address: "192.168.1.5"
})
%SnmpDevice{}
|> SnmpDevice.changeset(%{
device_id: device_without_hardware.id,
sys_name: "device",
sys_descr: "Device"
})
|> Towerops.Repo.insert!()
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/devices/#{device_without_hardware.id}?tab=hardware")
assert html =~ "No hardware inventory found"
end
test "switches to hardware 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", "Hardware") |> render_click()
assert html =~ "Class"
assert html =~ "Chassis 1"
end
end
end