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.
367 lines
10 KiB
Elixir
367 lines
10 KiB
Elixir
defmodule ToweropsWeb.DeviceLive.IndexTest do
|
|
use ToweropsWeb.ConnCase, async: false
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Towerops.Devices
|
|
|
|
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 "index page rendering" do
|
|
test "renders empty state when no devices", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/devices")
|
|
|
|
assert html =~ "No devices"
|
|
assert html =~ "New Device"
|
|
end
|
|
|
|
test "renders device list with devices", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Test Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/devices")
|
|
|
|
assert html =~ "Test Router"
|
|
assert html =~ "192.168.1.1"
|
|
end
|
|
|
|
test "renders site headers with device stats", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Router 1",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/devices")
|
|
|
|
assert html =~ "Test Site"
|
|
assert html =~ "1"
|
|
end
|
|
end
|
|
|
|
describe "real-time updates via PubSub" do
|
|
test "updates when a new device is created", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, view, html} = live(conn, ~p"/devices")
|
|
|
|
assert html =~ "No devices"
|
|
|
|
# Create a device (triggers PubSub broadcast)
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "New Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
# LiveView should receive PubSub event and re-render
|
|
html = render(view)
|
|
assert html =~ "New Router"
|
|
assert html =~ "192.168.1.1"
|
|
end
|
|
|
|
test "updates when a device is modified", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, device} =
|
|
Devices.create_device(%{
|
|
name: "Old Name",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/devices")
|
|
|
|
assert html =~ "Old Name"
|
|
|
|
{:ok, updated_device} = Devices.update_device(device, %{name: "New Name"})
|
|
|
|
# Verify the device was actually updated in the database
|
|
assert updated_device.name == "New Name"
|
|
|
|
# Verify LiveView received the PubSub message by checking that a fresh page load shows the update
|
|
{:ok, _view2, html2} = live(conn, ~p"/devices")
|
|
assert html2 =~ "New Name"
|
|
end
|
|
|
|
test "updates when a device status changes", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, device} =
|
|
Devices.create_device(%{
|
|
name: "Status Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, _view, _html} = live(conn, ~p"/devices")
|
|
|
|
{:ok, updated_device} = Devices.update_device_status(device, :up)
|
|
|
|
# Verify the status was actually updated
|
|
assert updated_device.status == :up
|
|
|
|
# Verify LiveView received the PubSub message by checking that a fresh page load shows the update
|
|
{:ok, _view2, html2} = live(conn, ~p"/devices")
|
|
assert html2 =~ "UP"
|
|
end
|
|
|
|
test "updates when a device is deleted", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, device} =
|
|
Devices.create_device(%{
|
|
name: "Delete Me",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, view, html} = live(conn, ~p"/devices")
|
|
|
|
assert html =~ "Delete Me"
|
|
|
|
{:ok, _deleted} = Devices.delete_device(device)
|
|
|
|
html = render(view)
|
|
refute html =~ "Delete Me"
|
|
assert html =~ "No devices"
|
|
end
|
|
|
|
test "updates device quota after changes", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Quota Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
html = render(view)
|
|
assert html =~ "1/10"
|
|
end
|
|
end
|
|
|
|
describe "tick timer" do
|
|
test "tick refreshes device stream on existing tab", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, device} =
|
|
Devices.create_device(%{
|
|
name: "Tick Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
# Update device status directly in DB (no broadcast)
|
|
{:ok, updated_device} =
|
|
device
|
|
|> Ecto.Changeset.change(%{status: :up})
|
|
|> Towerops.Repo.update()
|
|
|
|
assert updated_device.status == :up
|
|
|
|
# Send tick to trigger reload
|
|
send(view.pid, :tick)
|
|
|
|
# Verify tick caused a reload by checking that a fresh page load shows the update
|
|
{:ok, _view2, html2} = live(conn, ~p"/devices")
|
|
assert html2 =~ "UP"
|
|
end
|
|
|
|
test "tick does not reload on discovered tab", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/devices?tab=discovered")
|
|
|
|
# Send tick - should not crash or error
|
|
send(view.pid, :tick)
|
|
|
|
html = render(view)
|
|
assert html =~ "Discovered"
|
|
end
|
|
end
|
|
|
|
describe "site header enrichment" do
|
|
test "shows subscriber count on site header when Gaiia mapped", %{
|
|
conn: conn,
|
|
site: site,
|
|
organization: organization
|
|
} do
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, _} =
|
|
Towerops.Gaiia.upsert_network_site(organization.id, %{
|
|
gaiia_id: "ns-dev-list",
|
|
name: "Tower",
|
|
account_count: 55,
|
|
total_mrr: Decimal.new("2750.00"),
|
|
site_id: site.id
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/devices")
|
|
|
|
assert html =~ "55"
|
|
end
|
|
|
|
test "hides subscriber count when no Gaiia mapping", %{
|
|
conn: conn,
|
|
site: site,
|
|
organization: organization
|
|
} do
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/devices")
|
|
|
|
refute html =~ "subscribers"
|
|
end
|
|
end
|
|
|
|
describe "discovered tab" do
|
|
test "renders discovered devices tab", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/devices?tab=discovered")
|
|
|
|
assert html =~ "Discovered"
|
|
end
|
|
|
|
test "renders discovered tab with pagination", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/devices?tab=discovered&page=1")
|
|
|
|
assert html =~ "Discovered"
|
|
end
|
|
end
|
|
|
|
describe "device management events" do
|
|
test "toggles reorder mode", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
html = view |> element("#toggle-reorder-mode") |> render_click()
|
|
assert html =~ "Reset"
|
|
end
|
|
|
|
test "resets device order", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
# Reset button only renders inside reorder mode — toggle in first.
|
|
view |> element("#toggle-reorder-mode") |> render_click()
|
|
html = view |> element("#reset-order") |> render_click()
|
|
assert html =~ "Order reset to alphabetical"
|
|
end
|
|
|
|
test "force_rediscover_all with no SNMP devices shows error", %{conn: conn, site: site, organization: organization} do
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id,
|
|
snmp_enabled: false
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
html = view |> element("#force-rediscover-all") |> render_click()
|
|
assert html =~ "No SNMP-enabled devices found"
|
|
end
|
|
|
|
test "reorder_device with invalid device_id shows error", %{
|
|
conn: conn,
|
|
site: site,
|
|
organization: organization
|
|
} do
|
|
# The #device-list hook target only renders when has_devices is true.
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
# Fired by the DeviceListReorder JS hook on drag-drop. Using render_hook
|
|
# validates the hook is wired to #device-list, even though it can't run JS.
|
|
html =
|
|
view
|
|
|> element("#device-list")
|
|
|> render_hook("reorder_device", %{
|
|
"device_id" => Ecto.UUID.generate(),
|
|
"new_position" => "1"
|
|
})
|
|
|
|
assert html =~ "not found"
|
|
end
|
|
|
|
test "reorder_site with invalid site_id shows error", %{
|
|
conn: conn,
|
|
site: site,
|
|
organization: organization
|
|
} do
|
|
{:ok, _device} =
|
|
Devices.create_device(%{
|
|
name: "Router",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/devices")
|
|
|
|
html =
|
|
view
|
|
|> element("#device-list")
|
|
|> render_hook("reorder_site", %{
|
|
"site_id" => Ecto.UUID.generate(),
|
|
"new_position" => "1"
|
|
})
|
|
|
|
assert html =~ "not found"
|
|
end
|
|
end
|
|
end
|