towerops/test/towerops_web/live/agent_live/helpers_test.exs
Graham McIntire 872f717dba Fix critical bugs, memory leaks, and N+1 queries (#132)
CRITICAL FIXES:
- Fix unsafe pattern matching in SNMP discovery that would crash on timeout
  - Changed {:ok, _} = ... to proper case statements with error handling
  - Extracted safe_discover/3 helper to reduce cyclomatic complexity
  - Added fallback to empty lists and warning logging for timeouts
  - Affects: discover_vlans, discover_ip_addresses, discover_processors, discover_storage

MEMORY LEAK FIXES (JS Hooks):
- GlobalSearchTrigger: Added destroyed() cleanup for click listener
- SidebarCollapse: Added destroyed() cleanup for click listener
- ThemeSelector: Added destroyed() cleanup for phx:set-theme window listener
- NetworkMap: Added destroyed() cleanup for 3 button listeners (zoom in/out/fit)
- All hooks now properly store handler references and remove listeners on unmount

LIVEVIEW FIXES:
- Added phx-update="ignore" to 5 SensorChart hooks to prevent chart re-initialization
- Added catch-all handle_info(_msg, socket) to 5 LiveViews to prevent crashes on unexpected messages
  - device_live/show.ex, device_live/index.ex, alert_live/index.ex, activity_feed_live.ex, agent_live/index.ex

N+1 QUERY FIXES:
- Created Gaiia.get_site_subscriber_summaries/1 batch function
- Refactored alert_live and device_live to use batch query instead of looping
- Reduces queries from N to 1 when loading site subscriber data

Impact:
- Prevents discovery worker crashes during SNMP timeouts
- Eliminates memory accumulation from leaked event handlers
- Prevents chart state loss on LiveView updates
- Prevents LiveView crashes from unexpected PubSub messages
- Improves database performance by eliminating N+1 queries in alert/device views
- Passes credo --strict with 0 issues

Reviewed-on: graham/towerops-web#132
2026-03-23 17:03:41 -05:00

191 lines
5.7 KiB
Elixir

defmodule ToweropsWeb.AgentLive.HelpersTest do
use ExUnit.Case, async: true
alias ToweropsWeb.AgentLive.Helpers
describe "agent_status/1" do
test "returns :never when last_seen_at is nil" do
agent_token = %{last_seen_at: nil}
assert {:never, "Never connected"} = Helpers.agent_status(agent_token)
end
test "returns :online when last seen less than 120 seconds ago" do
last_seen = DateTime.add(DateTime.utc_now(), -60, :second)
agent_token = %{last_seen_at: last_seen}
assert {:online, "Online"} = Helpers.agent_status(agent_token)
end
test "returns :online at the boundary just under 120 seconds" do
last_seen = DateTime.add(DateTime.utc_now(), -118, :second)
agent_token = %{last_seen_at: last_seen}
assert {:online, "Online"} = Helpers.agent_status(agent_token)
end
test "returns :warning when last seen between 120 and 300 seconds ago" do
last_seen = DateTime.add(DateTime.utc_now(), -200, :second)
agent_token = %{last_seen_at: last_seen}
assert {:warning, "Warning"} = Helpers.agent_status(agent_token)
end
test "returns :warning at the boundary of exactly 120 seconds" do
last_seen = DateTime.add(DateTime.utc_now(), -120, :second)
agent_token = %{last_seen_at: last_seen}
assert {:warning, "Warning"} = Helpers.agent_status(agent_token)
end
test "returns :warning just under 300 seconds" do
last_seen = DateTime.add(DateTime.utc_now(), -299, :second)
agent_token = %{last_seen_at: last_seen}
assert {:warning, "Warning"} = Helpers.agent_status(agent_token)
end
test "returns :offline when last seen 300 or more seconds ago" do
last_seen = DateTime.add(DateTime.utc_now(), -300, :second)
agent_token = %{last_seen_at: last_seen}
assert {:offline, "Offline"} = Helpers.agent_status(agent_token)
end
test "returns :offline when last seen far in the past" do
last_seen = DateTime.add(DateTime.utc_now(), -86_400, :second)
agent_token = %{last_seen_at: last_seen}
assert {:offline, "Offline"} = Helpers.agent_status(agent_token)
end
end
describe "status_badge_class/1" do
test "returns green classes for :online" do
result = Helpers.status_badge_class(:online)
assert result =~ "bg-green-100"
assert result =~ "text-green-800"
end
test "returns yellow classes for :warning" do
result = Helpers.status_badge_class(:warning)
assert result =~ "bg-yellow-100"
assert result =~ "text-yellow-800"
end
test "returns red classes for :offline" do
result = Helpers.status_badge_class(:offline)
assert result =~ "bg-red-100"
assert result =~ "text-red-800"
end
test "returns gray classes for :never" do
result = Helpers.status_badge_class(:never)
assert result =~ "bg-gray-100"
assert result =~ "text-gray-800"
end
end
describe "status_dot_class/1" do
test "returns green with animation for :online" do
result = Helpers.status_dot_class(:online)
assert result =~ "bg-green-600"
assert result =~ "animate-pulse"
end
test "returns yellow without animation for :warning" do
result = Helpers.status_dot_class(:warning)
assert result =~ "bg-yellow-600"
refute result =~ "animate-pulse"
end
test "returns red without animation for :offline" do
result = Helpers.status_dot_class(:offline)
assert result =~ "bg-red-600"
refute result =~ "animate-pulse"
end
test "returns gray without animation for :never" do
result = Helpers.status_dot_class(:never)
assert result =~ "bg-gray-400"
refute result =~ "animate-pulse"
end
end
describe "format_uptime/1" do
test "formats days and hours when days > 0" do
# 2 days and 3 hours = 2*86400 + 3*3600 = 183600
assert "2d 3h" = Helpers.format_uptime(183_600)
end
test "formats days and zero hours" do
assert "1d 0h" = Helpers.format_uptime(86_400)
end
test "formats hours and minutes when hours > 0 and days == 0" do
# 2 hours and 34 minutes = 2*3600 + 34*60 = 9240
assert "2h 34m" = Helpers.format_uptime(9240)
end
test "formats only minutes when less than one hour" do
# 45 minutes = 45 * 60 = 2700
assert "45m" = Helpers.format_uptime(2700)
end
test "formats zero minutes" do
assert "0m" = Helpers.format_uptime(0)
end
test "formats small number of seconds as 0m" do
assert "0m" = Helpers.format_uptime(59)
end
test "returns Unknown for non-integer input" do
assert "Unknown" = Helpers.format_uptime(nil)
end
test "returns Unknown for string input" do
assert "Unknown" = Helpers.format_uptime("not a number")
end
test "returns Unknown for float input" do
assert "Unknown" = Helpers.format_uptime(123.45)
end
end
describe "format_agent_version/1" do
test "prepends v to semver string" do
assert "v1.2.3" = Helpers.format_agent_version("1.2.3")
end
test "prepends v to simple version" do
assert "v0.1.0" = Helpers.format_agent_version("0.1.0")
end
test "returns timestamp as-is when it starts with date prefix" do
timestamp = "2026-02-09T14:30:00Z"
assert ^timestamp = Helpers.format_agent_version(timestamp)
end
test "returns timestamp as-is for other RFC 3339 formats" do
timestamp = "2026-01-15T08:00:00+00:00"
assert ^timestamp = Helpers.format_agent_version(timestamp)
end
test "returns nil for nil input" do
assert nil == Helpers.format_agent_version(nil)
end
test "returns nil for non-binary input" do
assert nil == Helpers.format_agent_version(123)
end
end
end