towerops/test/towerops_web/live/device_live/index_test.exs
Graham McIntire 20f5f48372
feat: command center dashboard with unified insights and contextual enrichment
Transform the dashboard into a single-pane-of-glass command center that
blends operational metrics with business context from Gaiia subscriber data.

- Extend insight schema with multi-source support (snmp, gaiia, system)
- Add Oban workers for automated insight generation (device health, system, gaiia)
- New Dashboard context with health score computation and site summaries
- Rewrite dashboard LiveView with health overview, alert/insight feeds, site grid
- Add source filter pills for insights with URL state management
- Add metrics bar to site detail pages (device count, alerts, subscribers, MRR)
- Add subscriber/MRR context to alert list site links
- Add subscriber badges to device list site headers
- Real-time PubSub updates for alerts on dashboard
2026-02-13 14:52:49 -06:00

323 lines
8.8 KiB
Elixir

defmodule ToweropsWeb.DeviceLive.IndexTest do
use ToweropsWeb.ConnCase, async: true
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 device"
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} = Devices.update_device(device, %{name: "New Name"})
html = render(view)
assert html =~ "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} = Devices.update_device_status(device, :up)
html = render(view)
assert html =~ "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 devices"
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, _} =
device
|> Ecto.Changeset.change(%{status: :up})
|> Towerops.Repo.update()
# Send tick to trigger reload
send(view.pid, :tick)
html = render(view)
assert html =~ "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 subscribers"
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 = render_click(view, "toggle_reorder_mode")
assert html =~ "Reset Order"
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")
html = render_click(view, "reset_order")
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 = render_click(view, "force_rediscover_all")
assert html =~ "No SNMP-enabled devices found"
end
test "reorder_device with invalid device_id shows error", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/devices")
html =
render_click(view, "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} do
{:ok, view, _html} = live(conn, ~p"/devices")
html =
render_click(view, "reorder_site", %{
"site_id" => Ecto.UUID.generate(),
"new_position" => "1"
})
assert html =~ "not found"
end
end
end