## Summary - **`list_checks_for_agent` cascade**: the agent query only matched checks with `agent_token_id` set directly on the check record — checks on devices inheriting their agent via site/org/global default were never sent to the Go agent. Now uses left joins with the full cascade (device assignment → site → org default → global default cloud poller), matching the pattern from `list_agent_polling_targets`. - **At-risk summary double-counting**: `get_impact_summary` called `analyze_device_impact` per down AP, and each returned the full site subscriber count. With 3 APs down at a 147-sub site it showed 441 subs / $31k instead of 147 / $10k. Fixed by deduplicating impact per site. Same fix applied to `calculate_down_impact` in site impact summaries. ## Test plan - [x] 3 new cascade tests for `list_checks_for_agent` (site, org default, global default) - [x] 3 new tests for `get_impact_summary` (single site dedup, multi-site sum, zero case) - [x] All 8,607 existing tests pass - [ ] Deploy to staging, verify At Risk numbers on dashboard - [ ] Verify Go agent receives DNS/HTTP checks via cascade Reviewed-on: graham/towerops-web#79
276 lines
8.4 KiB
Elixir
276 lines
8.4 KiB
Elixir
defmodule Towerops.DashboardTest do
|
|
use Towerops.DataCase, async: true
|
|
|
|
import Towerops.AccountsFixtures
|
|
import Towerops.DevicesFixtures
|
|
import Towerops.OrganizationsFixtures
|
|
|
|
alias Towerops.Dashboard
|
|
alias Towerops.Gaiia
|
|
alias Towerops.Integrations
|
|
|
|
setup do
|
|
user = user_fixture()
|
|
org = organization_fixture(user.id)
|
|
|
|
%{org: org, user: user}
|
|
end
|
|
|
|
describe "get_dashboard_summary/1" do
|
|
test "returns correct structure with empty org", %{org: org} do
|
|
summary = Dashboard.get_dashboard_summary(org.id)
|
|
|
|
assert is_map(summary)
|
|
assert Map.has_key?(summary, :health_score)
|
|
assert Map.has_key?(summary, :devices)
|
|
assert Map.has_key?(summary, :alerts)
|
|
assert Map.has_key?(summary, :subscribers)
|
|
assert Map.has_key?(summary, :insights)
|
|
end
|
|
|
|
test "returns device counts", %{org: org} do
|
|
device = device_fixture(%{organization_id: org.id})
|
|
Towerops.Devices.update_device_status(device, :up)
|
|
|
|
summary = Dashboard.get_dashboard_summary(org.id)
|
|
|
|
assert summary.devices.total == 1
|
|
assert summary.devices.up == 1
|
|
assert summary.devices.down == 0
|
|
end
|
|
|
|
test "returns alert count", %{org: org} do
|
|
device = device_fixture(%{organization_id: org.id})
|
|
|
|
{:ok, _alert} =
|
|
Towerops.Alerts.create_alert(%{
|
|
device_id: device.id,
|
|
alert_type: "device_down",
|
|
triggered_at: DateTime.utc_now(),
|
|
message: "Device is down"
|
|
})
|
|
|
|
summary = Dashboard.get_dashboard_summary(org.id)
|
|
assert summary.alerts.active_count == 1
|
|
end
|
|
|
|
test "returns subscriber totals from billing integrations", %{org: org} do
|
|
{:ok, integration} =
|
|
Integrations.create_integration(org.id, %{provider: "gaiia", enabled: true})
|
|
|
|
{:ok, _} = Integrations.update_billing_totals(integration, 100, Decimal.new("5000.00"))
|
|
|
|
summary = Dashboard.get_dashboard_summary(org.id)
|
|
assert summary.subscribers.total == 100
|
|
assert Decimal.equal?(summary.subscribers.total_mrr, Decimal.new("5000.00"))
|
|
end
|
|
|
|
test "returns zero subscribers when no billing integrations", %{org: org} do
|
|
summary = Dashboard.get_dashboard_summary(org.id)
|
|
assert summary.subscribers.total == 0
|
|
end
|
|
|
|
test "returns insight counts by urgency", %{org: org} do
|
|
summary = Dashboard.get_dashboard_summary(org.id)
|
|
assert summary.insights.critical == 0
|
|
assert summary.insights.warning == 0
|
|
assert summary.insights.info == 0
|
|
end
|
|
end
|
|
|
|
describe "compute_health_score/1" do
|
|
test "returns 100 when all devices up, no alerts", %{org: org} do
|
|
device = device_fixture(%{organization_id: org.id})
|
|
Towerops.Devices.update_device_status(device, :up)
|
|
|
|
score = Dashboard.compute_health_score(org.id)
|
|
assert score == 100
|
|
end
|
|
|
|
test "returns lower score when devices down", %{org: org} do
|
|
d1 = device_fixture(%{organization_id: org.id})
|
|
d2 = device_fixture(%{organization_id: org.id})
|
|
Towerops.Devices.update_device_status(d1, :up)
|
|
Towerops.Devices.update_device_status(d2, :down)
|
|
|
|
score = Dashboard.compute_health_score(org.id)
|
|
assert score < 100
|
|
assert score > 0
|
|
end
|
|
|
|
test "returns 100 when no devices exist", %{org: org} do
|
|
score = Dashboard.compute_health_score(org.id)
|
|
assert score == 100
|
|
end
|
|
|
|
test "penalizes for critical alerts", %{org: org} do
|
|
device = device_fixture(%{organization_id: org.id})
|
|
Towerops.Devices.update_device_status(device, :up)
|
|
|
|
{:ok, _} =
|
|
Towerops.Alerts.create_alert(%{
|
|
device_id: device.id,
|
|
alert_type: "device_down",
|
|
triggered_at: DateTime.utc_now(),
|
|
message: "Device down"
|
|
})
|
|
|
|
score = Dashboard.compute_health_score(org.id)
|
|
assert score < 100
|
|
end
|
|
end
|
|
|
|
describe "get_org_subscriber_summary/1" do
|
|
test "returns totals from billing integrations", %{org: org} do
|
|
{:ok, i1} =
|
|
Integrations.create_integration(org.id, %{provider: "gaiia", enabled: true})
|
|
|
|
{:ok, _} = Integrations.update_billing_totals(i1, 50, Decimal.new("2500.00"))
|
|
|
|
{:ok, i2} =
|
|
Integrations.create_integration(org.id, %{provider: "splynx", enabled: true})
|
|
|
|
{:ok, _} = Integrations.update_billing_totals(i2, 30, Decimal.new("1500.00"))
|
|
|
|
result = Dashboard.get_org_subscriber_summary(org.id)
|
|
assert result.total == 80
|
|
assert Decimal.equal?(result.total_mrr, Decimal.new("4000.00"))
|
|
end
|
|
|
|
test "returns zeros when no billing integrations", %{org: org} do
|
|
result = Dashboard.get_org_subscriber_summary(org.id)
|
|
assert result.total == 0
|
|
assert Decimal.equal?(result.total_mrr, Decimal.new(0))
|
|
end
|
|
end
|
|
|
|
describe "get_impact_summary/1" do
|
|
test "does not multiply site subscribers by number of down APs", %{org: org} do
|
|
site = site_fixture(org.id)
|
|
|
|
{:ok, _network_site} =
|
|
Gaiia.upsert_network_site(org.id, %{
|
|
gaiia_id: "gaiia-site-1",
|
|
name: "Tower 3",
|
|
account_count: 147,
|
|
total_mrr: Decimal.new("10479.00")
|
|
})
|
|
|
|
Gaiia.update_network_site_mapping(
|
|
Gaiia.get_network_site(org.id, "gaiia-site-1"),
|
|
%{site_id: site.id}
|
|
)
|
|
|
|
# Create 3 APs at this site, all down
|
|
for i <- 1..3 do
|
|
device =
|
|
device_fixture(%{
|
|
organization_id: org.id,
|
|
site_id: site.id,
|
|
name: "AP #{i}"
|
|
})
|
|
|
|
Towerops.Devices.update_device(device, %{status: :down})
|
|
end
|
|
|
|
summary = Dashboard.get_impact_summary(org.id)
|
|
|
|
# Should be 147 subscribers (site total), NOT 147 * 3 = 441
|
|
assert summary.subscribers_affected == 147
|
|
assert Decimal.equal?(summary.mrr_at_risk, Decimal.new("10479.00"))
|
|
assert summary.down_device_count == 3
|
|
assert summary.sites_with_outages == 1
|
|
end
|
|
|
|
test "sums subscribers across different sites correctly", %{org: org} do
|
|
# Site 1 with 100 subscribers, 2 APs down
|
|
site1 = site_fixture(org.id, %{name: "Tower A"})
|
|
|
|
{:ok, _} =
|
|
Gaiia.upsert_network_site(org.id, %{
|
|
gaiia_id: "site-a",
|
|
name: "Tower A",
|
|
account_count: 100,
|
|
total_mrr: Decimal.new("5000.00")
|
|
})
|
|
|
|
Gaiia.update_network_site_mapping(
|
|
Gaiia.get_network_site(org.id, "site-a"),
|
|
%{site_id: site1.id}
|
|
)
|
|
|
|
for i <- 1..2 do
|
|
device =
|
|
device_fixture(%{
|
|
organization_id: org.id,
|
|
site_id: site1.id,
|
|
name: "Site1 AP #{i}"
|
|
})
|
|
|
|
Towerops.Devices.update_device(device, %{status: :down})
|
|
end
|
|
|
|
# Site 2 with 50 subscribers, 1 AP down
|
|
site2 = site_fixture(org.id, %{name: "Tower B"})
|
|
|
|
{:ok, _} =
|
|
Gaiia.upsert_network_site(org.id, %{
|
|
gaiia_id: "site-b",
|
|
name: "Tower B",
|
|
account_count: 50,
|
|
total_mrr: Decimal.new("2500.00")
|
|
})
|
|
|
|
Gaiia.update_network_site_mapping(
|
|
Gaiia.get_network_site(org.id, "site-b"),
|
|
%{site_id: site2.id}
|
|
)
|
|
|
|
device =
|
|
device_fixture(%{
|
|
organization_id: org.id,
|
|
site_id: site2.id,
|
|
name: "Site2 AP 1"
|
|
})
|
|
|
|
Towerops.Devices.update_device(device, %{status: :down})
|
|
|
|
summary = Dashboard.get_impact_summary(org.id)
|
|
|
|
# 100 + 50 = 150 total subscribers across 2 sites
|
|
assert summary.subscribers_affected == 150
|
|
assert Decimal.equal?(summary.mrr_at_risk, Decimal.new("7500.00"))
|
|
assert summary.down_device_count == 3
|
|
assert summary.sites_with_outages == 2
|
|
end
|
|
|
|
test "returns zero impact when no devices are down", %{org: org} do
|
|
summary = Dashboard.get_impact_summary(org.id)
|
|
|
|
assert summary.subscribers_affected == 0
|
|
assert Decimal.equal?(summary.mrr_at_risk, Decimal.new("0"))
|
|
assert summary.down_device_count == 0
|
|
assert summary.sites_with_outages == 0
|
|
end
|
|
end
|
|
|
|
describe "list_site_summaries/1" do
|
|
test "returns enriched site data", %{org: org} do
|
|
{:ok, site} =
|
|
Towerops.Sites.create_site(%{
|
|
name: "Test Tower",
|
|
organization_id: org.id
|
|
})
|
|
|
|
device = device_fixture(%{organization_id: org.id, site_id: site.id})
|
|
Towerops.Devices.update_device_status(device, :up)
|
|
|
|
summaries = Dashboard.list_site_summaries(org.id)
|
|
assert summaries != []
|
|
|
|
summary = Enum.find(summaries, &(&1.site_id == site.id))
|
|
assert summary.name == "Test Tower"
|
|
assert summary.device_count >= 1
|
|
end
|
|
end
|
|
end
|