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 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 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