towerops/test/towerops_web/live/dashboard_live_test.exs
Graham McIntire de075ea8d9
Store billing subscriber/MRR data on integrations for dashboard
Each billing sync (Gaiia, Splynx, VISP, Sonar) already computes
subscriber counts and MRR but only stored them in sync messages.
Now persists them on the integrations table so the dashboard can
aggregate across all billing providers instead of only querying
Gaiia network sites (which were never populated).

Also fixes SnmpKit Config environment detection to use Mix.env()
as fallback when MIX_ENV env var is not set.
2026-02-15 16:51:33 -06:00

406 lines
11 KiB
Elixir

defmodule ToweropsWeb.DashboardLiveTest do
use ToweropsWeb.ConnCase
import Phoenix.LiveViewTest
alias Towerops.Preseem.Insights
setup :register_and_log_in_user
setup %{conn: conn, user: user} do
{:ok, organization} =
Towerops.Organizations.create_organization(%{name: "Test Org", use_sites: true}, user.id)
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
conn = Plug.Conn.put_session(conn, :current_organization_id, organization.id)
%{conn: conn, organization: organization, site: site}
end
describe "empty state" do
test "shows onboarding when no devices exist", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/dashboard")
assert html =~ "Let's get started!"
assert html =~ "Create Your First Site"
end
test "requires authentication" do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/dashboard")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "health overview" do
test "displays health score", %{conn: conn, organization: organization, site: site} do
{:ok, device} = create_device(organization, site)
Towerops.Devices.update_device_status(device, :up)
{:ok, _view, html} = live(conn, ~p"/dashboard")
assert html =~ "Health"
assert html =~ "100"
end
test "displays device counts with up/down/unknown", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, d1} = create_device(organization, site, "Router 1", "192.168.1.1")
{:ok, d2} = create_device(organization, site, "Router 2", "192.168.1.2")
Towerops.Devices.update_device_status(d1, :up)
Towerops.Devices.update_device_status(d2, :down)
{:ok, _view, html} = live(conn, ~p"/dashboard")
assert html =~ "Devices"
assert html =~ "Up"
assert html =~ "Down"
end
test "displays active alert count", %{conn: conn, organization: organization, site: site} do
{:ok, device} = create_device(organization, site)
{:ok, _alert} =
Towerops.Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: DateTime.utc_now(),
message: "Device is down"
})
{:ok, _view, html} = live(conn, ~p"/dashboard")
assert html =~ "Active Issues"
assert html =~ "1"
end
test "displays insight counts by urgency", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, device} = create_device(organization, site)
{:ok, _} =
Insights.insert_insight_if_new(%{
organization_id: organization.id,
device_id: device.id,
type: "device_poll_gap",
source: "snmp",
urgency: "warning",
channel: "proactive",
title: "Device not polled"
})
{:ok, _view, html} = live(conn, ~p"/dashboard")
assert html =~ "Insights"
end
test "hides subscriber section when no Gaiia integration", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, _device} = create_device(organization, site)
{:ok, _view, html} = live(conn, ~p"/dashboard")
refute html =~ "Subscribers"
refute html =~ "MRR"
end
test "shows subscriber section when billing data exists", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, _device} = create_device(organization, site)
{:ok, integration} =
Towerops.Integrations.create_integration(organization.id, %{
provider: "gaiia",
enabled: true
})
{:ok, _} =
Towerops.Integrations.update_billing_totals(integration, 100, Decimal.new("5000.00"))
{:ok, _view, html} = live(conn, ~p"/dashboard")
assert html =~ "Subs"
assert html =~ "100"
assert html =~ "5,000"
end
end
describe "alert feed" do
test "shows alerts with device links", %{conn: conn, organization: organization, site: site} do
{:ok, device} = create_device(organization, site, "Main Router", "10.0.0.1")
Towerops.Devices.update_device_status(device, :down)
{:ok, _alert} =
Towerops.Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: DateTime.utc_now(),
message: "Device is unreachable"
})
{:ok, _view, html} = live(conn, ~p"/dashboard")
assert html =~ "Main Router"
assert html =~ "Device is unreachable"
assert html =~ ~p"/devices/#{device.id}"
end
test "shows View All link when alerts exist", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, device} = create_device(organization, site)
{:ok, _alert} =
Towerops.Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: DateTime.utc_now(),
message: "Device is down"
})
{:ok, _view, html} = live(conn, ~p"/dashboard")
# Alert feed appears when alerts exist
assert html =~ "Active Issues"
end
test "shows acknowledge button for unacknowledged alerts", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, device} = create_device(organization, site)
{:ok, _alert} =
Towerops.Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: DateTime.utc_now(),
message: "Device is down"
})
{:ok, _view, html} = live(conn, ~p"/dashboard")
assert html =~ "ACK"
end
end
describe "insight feed" do
test "shows insights from multiple sources", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, device} = create_device(organization, site)
{:ok, _} =
Insights.insert_insight_if_new(%{
organization_id: organization.id,
device_id: device.id,
type: "device_poll_gap",
source: "snmp",
urgency: "warning",
channel: "proactive",
title: "Device not polled in 24h"
})
{:ok, _view, html} = live(conn, ~p"/dashboard")
assert html =~ "Device not polled in 24h"
assert html =~ "snmp"
end
test "dismiss insight removes it from feed", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, device} = create_device(organization, site)
{:ok, insight} =
Insights.insert_insight_if_new(%{
organization_id: organization.id,
device_id: device.id,
type: "device_poll_gap",
source: "snmp",
urgency: "warning",
channel: "proactive",
title: "Poll gap detected"
})
{:ok, view, html} = live(conn, ~p"/dashboard")
assert html =~ "Poll gap detected"
view |> element("button[phx-value-id='#{insight.id}']") |> render_click()
html = render(view)
refute html =~ "Poll gap detected"
end
test "source filter filters insights via URL", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, device} = create_device(organization, site)
{:ok, _} =
Insights.insert_insight_if_new(%{
organization_id: organization.id,
device_id: device.id,
type: "device_poll_gap",
source: "snmp",
urgency: "warning",
channel: "proactive",
title: "SNMP insight"
})
{:ok, _} =
Insights.insert_insight_if_new(%{
organization_id: organization.id,
device_id: device.id,
type: "reconciliation_finding",
source: "gaiia",
urgency: "info",
channel: "proactive",
title: "Gaiia insight",
dedup_key: "test_dedup",
metadata: %{"dedup_key" => "test_dedup"}
})
{:ok, view, _html} = live(conn, ~p"/dashboard?insight_source=snmp")
html = render(view)
assert html =~ "SNMP insight"
refute html =~ "Gaiia insight"
end
end
describe "site health grid" do
test "shows site cards with device counts", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, device} = create_device(organization, site)
Towerops.Devices.update_device_status(device, :up)
{:ok, _view, html} = live(conn, ~p"/dashboard")
assert html =~ "Test Site"
assert html =~ ~p"/sites/#{site.id}"
end
test "shows subscriber data on site cards when Gaiia mapped", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, device} = create_device(organization, site)
Towerops.Devices.update_device_status(device, :up)
{:ok, _} =
Towerops.Gaiia.upsert_network_site(organization.id, %{
gaiia_id: "ns-site",
name: "Tower",
account_count: 42,
total_mrr: Decimal.new("2100.00"),
site_id: site.id
})
{:ok, _view, html} = live(conn, ~p"/dashboard")
assert html =~ "42"
end
end
describe "real-time updates" do
test "PubSub new alert event updates dashboard", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, device} = create_device(organization, site)
{:ok, view, _html} = live(conn, ~p"/dashboard")
# Create alert and broadcast
{:ok, _alert} =
Towerops.Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: DateTime.utc_now(),
message: "Device went down"
})
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:org:#{organization.id}:new",
{:new_alert, device.id, :device_down}
)
html = render(view)
assert html =~ "Device went down"
end
test "PubSub alert resolved event updates dashboard", %{
conn: conn,
organization: organization,
site: site
} do
{:ok, device} = create_device(organization, site)
{:ok, alert} =
Towerops.Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: DateTime.utc_now(),
message: "Device went down"
})
{:ok, view, _html} = live(conn, ~p"/dashboard")
# Resolve the alert
Towerops.Alerts.resolve_alert(alert)
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:org:#{organization.id}:resolved",
{:alert_resolved, device.id, :device_down}
)
html = render(view)
refute html =~ "Device went down"
end
end
defp create_device(organization, site, name \\ "Test Device", ip \\ "192.168.1.1") do
Towerops.Devices.create_device(%{
name: name,
ip_address: ip,
site_id: site.id,
organization_id: organization.id
})
end
end