towerops/test/towerops/dashboard_test.exs
Graham McIntire 0ac99f679c
fix: convert alert_type from enum to string and fix SQL array syntax
Two critical production bugs fixed:

1. **Alert type enum → string conversion**
   - Changed Alert.alert_type from Ecto.Enum to :string for flexibility
   - Updated all queries to use "device_down"/"device_up" strings instead of atoms
   - Fixed pattern matching in alerts.ex and device_monitor_worker.ex
   - Updated 44+ test files to use string literals

2. **SQL array indexing syntax error in activity feed**
   - Fixed PostgreSQL syntax: `array_agg(...)[1]` → `(array_agg(...))[1]`
   - Prevents "syntax error at or near [" in activity feed queries

3. **Added comprehensive timer cleanup tests**
   - Tests for mobile_qr_live.ex timer cleanup on terminate
   - Tests for agent_live index timer cleanup
   - Verifies memory leak fixes from previous commits

Files changed:
- lib/towerops/alerts.ex
- lib/towerops/alerts/alert.ex
- lib/towerops/activity_feed.ex
- lib/towerops/workers/device_monitor_worker.ex
- test/**/*_test.exs (44+ files with alert_type references)
- test/towerops_web/live/mobile_qr_live_test.exs
- test/towerops_web/live/agent_live_test.exs
- test/towerops/workers/device_poller_worker_test.exs

All tests passing except 1 unrelated brute force protection test.
2026-03-05 09:12:39 -06:00

165 lines
5.2 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.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 "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