test: GraphLive live mode + Show snmp_sensor format + Alert/Activity events
Bumps coverage by exercising several previously-uncovered branches: - GraphLive.Show: new test file driving `range=live` for every sensor_type (latency, processors, memory, traffic, temperature, storage, voltage, count, pppoe_sessions, connections, wireless, unknown), plus `get_title_suffix/1` across sensor_id / interface_id / storage_id, and `change_range` flipping into live mode. - DeviceLive.Show: format_check_value/2 now covers the snmp_sensor branch across temperature, voltage, current, power, frequency, humidity, fanspeed (default + custom unit), and an unknown sensor_type with/without a configured unit. - ActivityFeedLive: clear_search and load_more events; PubSub handle_info clauses for new_alert / alert_resolved / device_added / config_changed / sync_completed / device_event / :tick / catch-all. - AlertLive.Index: acknowledge / resolve event handlers across success, not-found, and cross-org unauthorized branches. Coverage: 86.42% → 86.52%.
This commit is contained in:
parent
fc20d60838
commit
332ee37706
4 changed files with 357 additions and 0 deletions
|
|
@ -50,6 +50,40 @@ defmodule ToweropsWeb.ActivityFeedLiveTest do
|
|||
_ = render_hook(view, "search", %{"search" => "router"})
|
||||
assert render(view) =~ "Activity"
|
||||
end
|
||||
|
||||
test "clear_search resets the search box", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/activity")
|
||||
_ = render_hook(view, "search", %{"search" => "anything"})
|
||||
_ = render_hook(view, "clear_search", %{})
|
||||
assert render(view) =~ "Activity"
|
||||
end
|
||||
|
||||
test "load_more bumps the limit and re-loads", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/activity")
|
||||
_ = render_hook(view, "load_more", %{})
|
||||
assert render(view) =~ "Activity"
|
||||
end
|
||||
end
|
||||
|
||||
describe "PubSub broadcasts trigger reloads" do
|
||||
test "new_alert / alert_resolved / device_added / config_changed / sync_completed / device_event / tick all keep the view alive",
|
||||
%{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/activity")
|
||||
|
||||
pid = view.pid
|
||||
|
||||
send(pid, {:new_alert, "device-1", "device_down"})
|
||||
send(pid, {:alert_resolved, "device-1", "device_down"})
|
||||
send(pid, {:device_added, "device-2"})
|
||||
send(pid, {:config_changed, "device-1"})
|
||||
send(pid, {:sync_completed, :ok})
|
||||
send(pid, {:device_event, "device-1", "interface_down"})
|
||||
send(pid, :tick)
|
||||
send(pid, {:unknown_message, :ignored})
|
||||
|
||||
# Force a render after the messages have been processed.
|
||||
assert render(view) =~ "Activity"
|
||||
end
|
||||
end
|
||||
|
||||
describe "severity / type helpers" do
|
||||
|
|
|
|||
|
|
@ -43,6 +43,69 @@ defmodule ToweropsWeb.AlertLive.IndexEventsTest do
|
|||
%{organization: organization, site: site, device: device, alert1: alert1, alert2: alert2}
|
||||
end
|
||||
|
||||
describe "acknowledge / resolve event handlers" do
|
||||
test "acknowledge flashes success and stamps the alert", %{
|
||||
conn: conn,
|
||||
alert1: alert1
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/alerts")
|
||||
|
||||
html = render_hook(view, "acknowledge", %{"id" => alert1.id})
|
||||
assert html =~ "acknowledged" or html =~ "Alert"
|
||||
|
||||
assert Alerts.get_alert!(alert1.id).acknowledged_at
|
||||
end
|
||||
|
||||
test "acknowledge with unknown id flashes 'not found'", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/alerts")
|
||||
html = render_hook(view, "acknowledge", %{"id" => Ecto.UUID.generate()})
|
||||
assert html =~ "not found" or html =~ "Alert"
|
||||
end
|
||||
|
||||
test "resolve flashes success and resolves the alert", %{
|
||||
conn: conn,
|
||||
alert1: alert1
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/alerts")
|
||||
_ = render_hook(view, "resolve", %{"id" => alert1.id})
|
||||
|
||||
assert Alerts.get_alert!(alert1.id).resolved_at
|
||||
end
|
||||
|
||||
test "resolve with unknown id flashes 'not found'", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/alerts")
|
||||
html = render_hook(view, "resolve", %{"id" => Ecto.UUID.generate()})
|
||||
assert html =~ "not found" or html =~ "Alert"
|
||||
end
|
||||
|
||||
test "acknowledge of an alert from another org is rejected as unauthorized", %{conn: conn} do
|
||||
# Create a foreign alert outside the user's org.
|
||||
foreign_user = Towerops.AccountsFixtures.user_fixture()
|
||||
foreign_org = Towerops.OrganizationsFixtures.organization_fixture(foreign_user.id)
|
||||
{:ok, foreign_site} = Towerops.Sites.create_site(%{name: "FS", organization_id: foreign_org.id})
|
||||
|
||||
{:ok, foreign_device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Foreign",
|
||||
ip_address: "10.99.99.99",
|
||||
site_id: foreign_site.id,
|
||||
organization_id: foreign_org.id
|
||||
})
|
||||
|
||||
{:ok, foreign_alert} =
|
||||
Alerts.create_alert(%{
|
||||
device_id: foreign_device.id,
|
||||
alert_type: "device_down",
|
||||
triggered_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
severity: 1
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/alerts")
|
||||
html = render_hook(view, "acknowledge", %{"id" => foreign_alert.id})
|
||||
assert html =~ "access" or html =~ "not found" or html =~ "Alert"
|
||||
end
|
||||
end
|
||||
|
||||
describe "toggle_alert" do
|
||||
test "expands and collapses an alert row", %{conn: conn, alert1: alert1} do
|
||||
{:ok, view, _html} = live(conn, ~p"/alerts")
|
||||
|
|
|
|||
|
|
@ -121,5 +121,56 @@ defmodule ToweropsWeb.DeviceLive.ShowHelpersTest do
|
|||
test "unknown check_type rounds to 2 decimals" do
|
||||
assert "42.5" == Show.format_check_value(%{value: 42.5}, %{check_type: "weird"})
|
||||
end
|
||||
|
||||
test "snmp_sensor with temperature uses 1 decimal + °C" do
|
||||
check = %{check_type: "snmp_sensor", config: %{"sensor_type" => "temperature"}}
|
||||
assert "23.5°C" == Show.format_check_value(%{value: 23.456}, check)
|
||||
end
|
||||
|
||||
test "snmp_sensor with voltage uses 2 decimals + V" do
|
||||
check = %{check_type: "snmp_sensor", config: %{"sensor_type" => "voltage"}}
|
||||
assert "12.34V" == Show.format_check_value(%{value: 12.341}, check)
|
||||
end
|
||||
|
||||
test "snmp_sensor with current uses 2 decimals + A" do
|
||||
check = %{check_type: "snmp_sensor", config: %{"sensor_type" => "current"}}
|
||||
result = Show.format_check_value(%{value: 1.523}, check)
|
||||
assert result == "1.52A"
|
||||
end
|
||||
|
||||
test "snmp_sensor with power uses 1 decimal + W" do
|
||||
check = %{check_type: "snmp_sensor", config: %{"sensor_type" => "power"}}
|
||||
assert "75.5W" == Show.format_check_value(%{value: 75.456}, check)
|
||||
end
|
||||
|
||||
test "snmp_sensor with frequency uses 0 decimals + Hz" do
|
||||
check = %{check_type: "snmp_sensor", config: %{"sensor_type" => "frequency"}}
|
||||
assert "60.0Hz" == Show.format_check_value(%{value: 60.0}, check)
|
||||
end
|
||||
|
||||
test "snmp_sensor with humidity uses 1 decimal + %" do
|
||||
check = %{check_type: "snmp_sensor", config: %{"sensor_type" => "humidity"}}
|
||||
assert "55.5%" == Show.format_check_value(%{value: 55.55}, check)
|
||||
end
|
||||
|
||||
test "snmp_sensor with fanspeed uses RPM by default" do
|
||||
check = %{check_type: "snmp_sensor", config: %{"sensor_type" => "fanspeed", "sensor_unit" => nil}}
|
||||
assert "3000 RPM" == Show.format_check_value(%{value: 3000}, check)
|
||||
end
|
||||
|
||||
test "snmp_sensor with fanspeed honors a configured unit" do
|
||||
check = %{check_type: "snmp_sensor", config: %{"sensor_type" => "fanspeed", "sensor_unit" => "rpm"}}
|
||||
assert "3000rpm" == Show.format_check_value(%{value: 3000}, check)
|
||||
end
|
||||
|
||||
test "snmp_sensor with unknown sensor_type uses default 2-decimal precision and the configured unit" do
|
||||
check = %{check_type: "snmp_sensor", config: %{"sensor_type" => "lux", "sensor_unit" => "lx"}}
|
||||
assert "123.46lx" == Show.format_check_value(%{value: 123.456}, check)
|
||||
end
|
||||
|
||||
test "snmp_sensor with unknown sensor_type and no unit defaults to no unit suffix" do
|
||||
check = %{check_type: "snmp_sensor", config: %{"sensor_type" => "lux"}}
|
||||
assert "123.46" == Show.format_check_value(%{value: 123.456}, check)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
209
test/towerops_web/live/graph_live/show_live_mode_test.exs
Normal file
209
test/towerops_web/live/graph_live/show_live_mode_test.exs
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
defmodule ToweropsWeb.GraphLive.ShowLiveModeTest do
|
||||
@moduledoc """
|
||||
Coverage for the `range=live` (live polling) entry path of GraphLive.Show.
|
||||
Drives `load_live_mode_graph_data/1` and `get_title_suffix/1` across the
|
||||
sensor / interface / storage suffix branches.
|
||||
|
||||
We don't actually poll SNMP — the live test just needs to mount, which
|
||||
exercises the live-mode setup branches (`is_live_mode == true`) plus the
|
||||
per-sensor-type chart_config lookups.
|
||||
"""
|
||||
use ToweropsWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Towerops.Organizations
|
||||
alias Towerops.Repo
|
||||
alias Towerops.Sites
|
||||
alias Towerops.Snmp
|
||||
alias Towerops.Snmp.Device, as: SnmpDevice
|
||||
alias Towerops.Snmp.Interface
|
||||
alias Towerops.Snmp.Sensor
|
||||
alias Towerops.Snmp.Storage
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
setup %{user: user} do
|
||||
{:ok, organization} = Organizations.create_organization(%{name: "Live Org"}, user.id)
|
||||
{:ok, site} = Sites.create_site(%{name: "Live Site", organization_id: organization.id})
|
||||
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Live Router",
|
||||
ip_address: "10.50.0.1",
|
||||
site_id: site.id,
|
||||
organization_id: organization.id,
|
||||
snmp_enabled: true
|
||||
})
|
||||
|
||||
snmp_device =
|
||||
%SnmpDevice{}
|
||||
|> SnmpDevice.changeset(%{device_id: device.id, sys_name: "live-router"})
|
||||
|> Repo.insert!()
|
||||
|
||||
%{organization: organization, site: site, device: device, snmp_device: snmp_device}
|
||||
end
|
||||
|
||||
describe "live mode entry (range=live)" do
|
||||
test "renders live processors graph", %{conn: conn, device: device} do
|
||||
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/processors?range=live")
|
||||
assert html =~ "Processor Usage"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
|
||||
test "renders live memory graph", %{conn: conn, device: device} do
|
||||
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/memory?range=live")
|
||||
assert html =~ "Memory Usage"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
|
||||
test "renders live latency graph", %{conn: conn, device: device} do
|
||||
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/latency?range=live")
|
||||
assert html =~ "Ping Latency"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
|
||||
test "renders live traffic graph (overall device)", %{conn: conn, device: device} do
|
||||
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/traffic?range=live")
|
||||
assert html =~ "Traffic"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
|
||||
test "renders live temperature graph", %{conn: conn, device: device} do
|
||||
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/temperature?range=live")
|
||||
assert html =~ "Temperature"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
|
||||
test "renders live storage graph", %{conn: conn, device: device} do
|
||||
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/storage?range=live")
|
||||
assert html =~ "Storage"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
|
||||
test "renders live wireless graph (single-token branch in get_chart_config)", %{conn: conn, device: device} do
|
||||
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/wireless?range=live")
|
||||
assert html =~ "Wireless"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
|
||||
test "renders live count graph", %{conn: conn, device: device} do
|
||||
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/count?range=live")
|
||||
assert html =~ "Count"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
|
||||
test "renders live pppoe_sessions graph", %{conn: conn, device: device} do
|
||||
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/pppoe_sessions?range=live")
|
||||
assert html =~ "PPPoE"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
|
||||
test "renders live connections graph", %{conn: conn, device: device} do
|
||||
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/connections?range=live")
|
||||
assert html =~ "Connections"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
|
||||
test "renders live voltage graph", %{conn: conn, device: device} do
|
||||
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/voltage?range=live")
|
||||
assert html =~ "Voltage"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
|
||||
test "renders live unknown sensor type with humanized title", %{conn: conn, device: device} do
|
||||
{:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/graph/some_custom_metric?range=live")
|
||||
# humanize_sensor_type/1 turns "some_custom_metric" → "Some Custom Metric"
|
||||
assert html =~ "Some Custom Metric"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
end
|
||||
|
||||
describe "live mode title suffixes (covers get_title_suffix/1 branches)" do
|
||||
test "live mode with sensor_id appends sensor description to title", %{
|
||||
conn: conn,
|
||||
device: device,
|
||||
snmp_device: snmp_device
|
||||
} do
|
||||
sensor =
|
||||
%Sensor{}
|
||||
|> Sensor.changeset(%{
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_type: "voltage",
|
||||
sensor_index: "1",
|
||||
sensor_oid: "1.3.6.1.4.1.9.9.91.1.1.1.1.4.1",
|
||||
sensor_descr: "PSU-1 Voltage",
|
||||
sensor_unit: "V"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
{:ok, _view, html} =
|
||||
live(conn, ~p"/devices/#{device.id}/graph/voltage?range=live&sensor_id=#{sensor.id}")
|
||||
|
||||
assert html =~ "PSU-1 Voltage"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
|
||||
test "live mode with interface_id appends interface name (traffic)", %{
|
||||
conn: conn,
|
||||
device: device,
|
||||
snmp_device: snmp_device
|
||||
} do
|
||||
iface =
|
||||
%Interface{}
|
||||
|> Interface.changeset(%{
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_name: "ether1",
|
||||
if_phys_address: "aa:bb:cc:00:00:01"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
{:ok, _view, html} =
|
||||
live(conn, ~p"/devices/#{device.id}/graph/traffic?range=live&interface_id=#{iface.id}")
|
||||
|
||||
assert html =~ "ether1"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
|
||||
test "live mode with storage_id appends storage description (storage_volume)", %{
|
||||
conn: conn,
|
||||
device: device,
|
||||
snmp_device: snmp_device
|
||||
} do
|
||||
storage =
|
||||
%Storage{}
|
||||
|> Storage.changeset(%{
|
||||
snmp_device_id: snmp_device.id,
|
||||
storage_index: "1",
|
||||
storage_oid: "1.3.6.1.2.1.25.2.3.1.1",
|
||||
description: "/data",
|
||||
storage_type: "fixed_disk"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
{:ok, _view, html} =
|
||||
live(
|
||||
conn,
|
||||
~p"/devices/#{device.id}/graph/storage_volume?range=live&storage_id=#{storage.id}"
|
||||
)
|
||||
|
||||
assert html =~ "/data"
|
||||
assert html =~ "Live"
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_range to live" do
|
||||
test "switching to range=live patches the URL and flips into live mode", %{
|
||||
conn: conn,
|
||||
device: device
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/graph/processors?range=24h")
|
||||
|
||||
assert render_click(view, "change_range", %{"range" => "live"}) =~ "Live"
|
||||
end
|
||||
end
|
||||
|
||||
# Silence unused-alias warning for Snmp.
|
||||
_ = Snmp
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue