towerops/test/towerops_web/live/network_map_live_test.exs
Graham McIntire 80304329bd
improve network map topology visualization
- fix bidirectional link queries to find links where device is source or target
- merge A→B and B→A edges into single link with combined confidence
- include interface speed in edge data for variable edge width
- infer discovered node roles from LLDP/CDP capabilities (router, switch, wireless, phone)
- store remote_capabilities in link metadata during evidence processing
- replace Cytoscape destroy-and-recreate with element diffing to preserve zoom/pan/positions
- add node detail slide-out panel with device info, connections, and confidence
- support ?node= URL param for deep-linking to selected nodes
- add site-based compound node grouping in Cytoscape
2026-03-12 10:59:20 -05:00

228 lines
6.6 KiB
Elixir

defmodule ToweropsWeb.NetworkMapLiveTest do
use ToweropsWeb.ConnCase, async: true
import Phoenix.LiveViewTest
setup :register_and_log_in_user
setup %{user: user} do
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
%{organization: organization}
end
describe "mount and render" do
test "mounts and renders the network map page", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/network-map")
assert html =~ "Network Map"
assert html =~ "Experimental"
assert html =~ "Topology Visualization"
end
test "shows stats bar with default zeros", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/network-map")
assert html =~ "Total Devices"
assert html =~ "Added Devices"
assert html =~ "Discovered"
assert html =~ "Connections"
end
test "shows tab navigation with added and all tabs", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/network-map")
assert html =~ "Added Devices"
assert html =~ "All Devices"
end
test "shows empty state when no devices exist", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/network-map")
assert html =~ "No network topology available"
assert html =~ "Add devices"
end
test "shows refresh button", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/network-map")
assert html =~ "Refresh"
end
test "shows legend with role and confidence indicators", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/network-map")
assert html =~ "Roles:"
assert html =~ "Router"
assert html =~ "Switch"
assert html =~ "Firewall"
assert html =~ "Discovered"
assert html =~ "Links:"
assert html =~ "High confidence"
end
test "shows cytoscape container with network map hook", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/network-map")
assert html =~ "cy-container"
assert html =~ "NetworkMap"
end
end
describe "tab navigation" do
test "defaults to added tab", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/network-map")
# The "Added Devices" tab should have active styling
assert html =~ "Added Devices"
end
test "switches to all tab via params", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/network-map?tab=all")
assert html =~ "All Devices"
end
end
describe "events" do
test "refresh_topology reloads data and shows flash", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/network-map")
html = render_click(view, "refresh_topology")
assert html =~ "Network map refreshed"
end
end
describe "topology update events" do
test "pushes update_topology event on mount", %{conn: conn, organization: organization} do
{:ok, site} = Towerops.Sites.create_site(%{name: "Test Site", organization_id: organization.id})
{:ok, _device} =
Towerops.Devices.create_device(
%{
name: "Test-Router",
ip_address: "10.0.0.1",
snmp_enabled: true,
snmp_version: "2c",
snmp_community: "public",
snmp_port: 161,
site_id: site.id,
organization_id: organization.id
},
bypass_limits: true
)
{:ok, view, _html} = live(conn, ~p"/network-map")
# Verify the topology data is loaded (not empty)
topology = view |> element("#cy-container") |> render()
assert topology =~ "data-topology"
end
test "refreshes topology on PubSub message", %{conn: conn, organization: organization} do
{:ok, view, _html} = live(conn, ~p"/network-map")
# Broadcast a topology update
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"topology:#{organization.id}",
{:topology_updated, organization.id}
)
# Give LiveView time to process
_ = render(view)
# Verify it still renders without error
html = render(view)
assert html =~ "Topology Visualization"
end
end
describe "node detail panel" do
setup %{organization: organization} do
{:ok, site} = Towerops.Sites.create_site(%{name: "Test Site", organization_id: organization.id})
{:ok, device} =
Towerops.Devices.create_device(
%{
name: "Test-Router",
ip_address: "10.0.0.1",
snmp_enabled: true,
snmp_version: "2c",
snmp_community: "public",
snmp_port: 161,
site_id: site.id,
organization_id: organization.id
},
bypass_limits: true
)
%{site: site, device: device}
end
test "node_clicked event assigns selected node detail", %{conn: conn, device: device} do
{:ok, view, _html} = live(conn, ~p"/network-map")
html = render_click(view, "node_clicked", %{"node_id" => device.id, "node_type" => "managed"})
assert html =~ "Test-Router"
assert html =~ "10.0.0.1"
end
test "close button clears selection", %{conn: conn, device: device} do
{:ok, view, _html} = live(conn, ~p"/network-map")
# Open panel
render_click(view, "node_clicked", %{"node_id" => device.id, "node_type" => "managed"})
# Close panel
html = render_click(view, "close_detail_panel")
refute html =~ "Node Details"
end
test "?node=<id> param opens detail panel on mount", %{conn: conn, device: device} do
{:ok, _view, html} = live(conn, ~p"/network-map?node=#{device.id}")
assert html =~ "Test-Router"
assert html =~ "10.0.0.1"
end
end
describe "site grouping" do
test "network map renders site groupings in topology data", %{conn: conn, organization: organization} do
{:ok, site} = Towerops.Sites.create_site(%{name: "Main Office", organization_id: organization.id})
{:ok, _device} =
Towerops.Devices.create_device(
%{
name: "Office-Router",
ip_address: "10.0.0.1",
snmp_enabled: true,
snmp_version: "2c",
snmp_community: "public",
snmp_port: 161,
site_id: site.id,
organization_id: organization.id
},
bypass_limits: true
)
{:ok, _view, html} = live(conn, ~p"/network-map")
# The topology data should include site info in JSON
assert html =~ "Main Office"
end
end
describe "unauthenticated access" do
test "requires authentication" do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/network-map")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
end