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.
406 lines
11 KiB
Elixir
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
|