Add comprehensive tests for Towerops.Monitoring.DeviceMonitor
- Test start_link/1 and Horde Registry registration - Test initial check on monitoring enabled/disabled - Test trigger_check/1 for running and stopped monitors - Test successful and failed ping handling - Test status change detection (up->down, down->up) - Test alert creation and resolution - Test PubSub broadcasts for status changes and alerts - Test alert message formatting for ICMP vs SNMP - Test check scheduling based on check_interval_seconds - Coverage improved from 43.93% to 96.67% Total tests: 1066 -> 1089 (23 new tests) Total coverage: 48.72% -> 49.65%
This commit is contained in:
parent
f9553b1444
commit
f5ff6c5c61
1 changed files with 471 additions and 0 deletions
471
test/towerops/monitoring/device_monitor_test.exs
Normal file
471
test/towerops/monitoring/device_monitor_test.exs
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
defmodule Towerops.Monitoring.DeviceMonitorTest do
|
||||
use Towerops.DataCase, async: false
|
||||
|
||||
import Mox
|
||||
import Swoosh.TestAssertions
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Alerts
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Monitoring
|
||||
alias Towerops.Monitoring.DeviceMonitor
|
||||
alias Towerops.Monitoring.PingMock
|
||||
|
||||
setup :verify_on_exit!
|
||||
setup :set_mox_global
|
||||
|
||||
setup do
|
||||
# Use stub_with for GenServer tests since monitors may ping multiple times
|
||||
stub_with(PingMock, Towerops.Monitoring.PingStub)
|
||||
|
||||
# Start the Monitoring.Supervisor for these tests
|
||||
start_supervised!(Towerops.Monitoring.Supervisor)
|
||||
|
||||
# Create test data
|
||||
user = user_fixture()
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, device} =
|
||||
Devices.create_device(%{
|
||||
name: "Test Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
monitoring_enabled: true,
|
||||
check_interval_seconds: 30
|
||||
})
|
||||
|
||||
# Consume confirmation email from user creation
|
||||
assert_email_sent(subject: "Confirmation instructions")
|
||||
|
||||
%{device: device, site: site, organization: organization}
|
||||
end
|
||||
|
||||
describe "start_link/1" do
|
||||
test "starts a monitor GenServer for a device", %{device: device} do
|
||||
assert {:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
assert is_pid(pid)
|
||||
assert Process.alive?(pid)
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "registers monitor in Horde Registry", %{device: device} do
|
||||
{:ok, _pid} = DeviceMonitor.start_link(device.id)
|
||||
|
||||
assert [{pid, _}] = Horde.Registry.lookup(Towerops.Monitoring.Registry, device.id)
|
||||
assert Process.alive?(pid)
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "init/1" do
|
||||
test "performs immediate check when monitoring is enabled", %{device: device} do
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
|
||||
# Give it time to perform the initial check
|
||||
Process.sleep(50)
|
||||
|
||||
# Verify check was created
|
||||
checks = Monitoring.list_devices_checks(device.id)
|
||||
assert length(checks) >= 1
|
||||
assert hd(checks).status == :success
|
||||
assert hd(checks).response_time_ms == 10
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "does not check when monitoring is disabled", %{device: device} do
|
||||
# Disable monitoring
|
||||
{:ok, device} = Devices.update_device(device, %{monitoring_enabled: false})
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
# Verify no checks were created
|
||||
checks = Monitoring.list_devices_checks(device.id)
|
||||
assert checks == []
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "trigger_check/1" do
|
||||
test "triggers immediate check when monitor is running", %{device: device} do
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
# Count initial checks
|
||||
initial_count = length(Monitoring.list_devices_checks(device.id))
|
||||
|
||||
DeviceMonitor.trigger_check(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
# Should have at least one more check
|
||||
final_count = length(Monitoring.list_devices_checks(device.id))
|
||||
assert final_count > initial_count
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "enqueues check job when monitor is not running", %{device: device} do
|
||||
# Disable monitoring so monitor doesn't start
|
||||
{:ok, device} = Devices.update_device(device, %{monitoring_enabled: false})
|
||||
|
||||
DeviceMonitor.trigger_check(device.id)
|
||||
Process.sleep(100)
|
||||
|
||||
# Verify check was created
|
||||
checks = Monitoring.list_devices_checks(device.id)
|
||||
assert length(checks) == 1
|
||||
assert hd(checks).response_time_ms == 10
|
||||
end
|
||||
end
|
||||
|
||||
describe "perform_check/1 - successful ping" do
|
||||
test "creates successful monitoring check", %{device: device} do
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
check = Monitoring.get_latest_check(device.id)
|
||||
assert check.status == :success
|
||||
assert check.response_time_ms == 10
|
||||
assert check.device_id == device.id
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "updates device status to :up", %{device: device} do
|
||||
# Start with device down
|
||||
{:ok, device} = Devices.update_device_status(device, :down)
|
||||
assert device.status == :down
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
# Device should be up now
|
||||
device = Devices.get_device!(device.id)
|
||||
assert device.status == :up
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "broadcasts device status change via PubSub", %{device: device} do
|
||||
# Subscribe to device status changes
|
||||
Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}")
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
|
||||
# Wait for broadcast
|
||||
assert_receive {:device_status_changed, device_id, :up, nil}, 200
|
||||
assert device_id == device.id
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "perform_check/1 - failed ping" do
|
||||
setup do
|
||||
# Override stub to return errors for this describe block
|
||||
stub(PingMock, :ping, fn _ -> {:error, :timeout} end)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "creates failed monitoring check", %{device: device} do
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
check = Monitoring.get_latest_check(device.id)
|
||||
assert check.status == :failure
|
||||
assert is_nil(check.response_time_ms)
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "updates device status to :down", %{device: device} do
|
||||
# Start with device up
|
||||
{:ok, device} = Devices.update_device_status(device, :up)
|
||||
assert device.status == :up
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
# Device should be down now
|
||||
device = Devices.get_device!(device.id)
|
||||
assert device.status == :down
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "status change - up to down" do
|
||||
setup do
|
||||
stub(PingMock, :ping, fn _ -> {:error, :timeout} end)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "creates device_down alert", %{device: device} do
|
||||
# Start with device up
|
||||
{:ok, device} = Devices.update_device_status(device, :up)
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
# Check alert was created
|
||||
assert Alerts.has_active_alert?(device.id, :device_down)
|
||||
alert = Alerts.get_active_alert(device.id, :device_down)
|
||||
assert alert.alert_type == :device_down
|
||||
assert alert.message =~ "not responding"
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "broadcasts new_alert via PubSub", %{device: device} do
|
||||
{:ok, _device} = Devices.update_device_status(device, :up)
|
||||
|
||||
# Subscribe to alerts
|
||||
Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:new")
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
|
||||
# Wait for broadcast
|
||||
assert_receive {:new_alert, device_id, :device_down}, 200
|
||||
assert device_id == device.id
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "does not create duplicate alerts when already down", %{device: device} do
|
||||
{:ok, device} = Devices.update_device_status(device, :up)
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
# Second check - should not create duplicate
|
||||
GenServer.cast(pid, :check_now)
|
||||
Process.sleep(50)
|
||||
|
||||
# Only one active alert should exist
|
||||
alerts = Alerts.list_devices_alerts(device.id)
|
||||
down_alerts = Enum.filter(alerts, &(&1.alert_type == :device_down and is_nil(&1.resolved_at)))
|
||||
assert length(down_alerts) == 1
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "status change - down to up" do
|
||||
test "creates device_up alert", %{device: device} do
|
||||
# Start with device down and create down alert
|
||||
{:ok, device} = Devices.update_device_status(device, :down)
|
||||
|
||||
{:ok, _alert} =
|
||||
Alerts.create_alert(%{
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
# Check recovery alert was created
|
||||
alerts = Alerts.list_devices_alerts(device.id)
|
||||
up_alerts = Enum.filter(alerts, &(&1.alert_type == :device_up))
|
||||
assert length(up_alerts) >= 1
|
||||
up_alert = hd(up_alerts)
|
||||
assert up_alert.message =~ "now responding"
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "resolves existing device_down alert", %{device: device} do
|
||||
{:ok, device} = Devices.update_device_status(device, :down)
|
||||
|
||||
# Create down alert
|
||||
{:ok, down_alert} =
|
||||
Alerts.create_alert(%{
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
# Down alert should be resolved
|
||||
refute Alerts.has_active_alert?(device.id, :device_down)
|
||||
resolved_alert = Alerts.get_alert!(down_alert.id)
|
||||
assert resolved_alert.resolved_at
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "broadcasts alert_resolved via PubSub", %{device: device} do
|
||||
{:ok, _device} = Devices.update_device_status(device, :down)
|
||||
|
||||
# Create existing down alert
|
||||
{:ok, _alert} =
|
||||
Alerts.create_alert(%{
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
# Subscribe to resolved alerts
|
||||
Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:resolved")
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
|
||||
# Wait for broadcast
|
||||
assert_receive {:alert_resolved, device_id, :device_down}, 200
|
||||
assert device_id == device.id
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "alert messages" do
|
||||
setup do
|
||||
stub(PingMock, :ping, fn _ -> {:error, :timeout} end)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "uses ICMP message when SNMP is disabled", %{device: device} do
|
||||
{:ok, device} = Devices.update_device(device, %{snmp_enabled: false})
|
||||
{:ok, _device} = Devices.update_device_status(device, :up)
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
alert = Alerts.get_active_alert(device.id, :device_down)
|
||||
assert alert.message == "Device is not responding to ping"
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "uses SNMP message when SNMP is enabled", %{device: device} do
|
||||
{:ok, device} = Devices.update_device(device, %{snmp_enabled: true})
|
||||
{:ok, _device} = Devices.update_device_status(device, :up)
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
alert = Alerts.get_active_alert(device.id, :device_down)
|
||||
assert alert.message == "Device is not responding to SNMP"
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "alert messages - recovery" do
|
||||
test "recovery message mentions ICMP when SNMP disabled", %{device: device} do
|
||||
{:ok, device} = Devices.update_device(device, %{snmp_enabled: false})
|
||||
{:ok, device} = Devices.update_device_status(device, :down)
|
||||
|
||||
{:ok, _alert} =
|
||||
Alerts.create_alert(%{
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
alerts = Alerts.list_devices_alerts(device.id)
|
||||
up_alert = Enum.find(alerts, &(&1.alert_type == :device_up))
|
||||
assert up_alert.message == "Device is now responding to ping"
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "recovery message mentions SNMP when SNMP enabled", %{device: device} do
|
||||
{:ok, device} = Devices.update_device(device, %{snmp_enabled: true})
|
||||
{:ok, device} = Devices.update_device_status(device, :down)
|
||||
|
||||
{:ok, _alert} =
|
||||
Alerts.create_alert(%{
|
||||
device_id: device.id,
|
||||
alert_type: :device_down,
|
||||
triggered_at: DateTime.utc_now(),
|
||||
message: "Device is down"
|
||||
})
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
alerts = Alerts.list_devices_alerts(device.id)
|
||||
up_alert = Enum.find(alerts, &(&1.alert_type == :device_up))
|
||||
assert up_alert.message == "Device is now responding to SNMP"
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "scheduling" do
|
||||
test "schedules next check based on check_interval_seconds", %{device: device} do
|
||||
# Set short interval for testing
|
||||
{:ok, device} = Devices.update_device(device, %{check_interval_seconds: 1})
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
|
||||
# Wait for initial check
|
||||
Process.sleep(50)
|
||||
|
||||
# Wait for scheduled check (1 second interval)
|
||||
Process.sleep(1100)
|
||||
|
||||
# Should have at least 2 checks
|
||||
checks = Monitoring.list_devices_checks(device.id, 10)
|
||||
assert length(checks) >= 2
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
|
||||
test "does not schedule next check when monitoring disabled", %{device: device} do
|
||||
{:ok, device} = Devices.update_device(device, %{monitoring_enabled: false})
|
||||
|
||||
{:ok, pid} = DeviceMonitor.start_link(device.id)
|
||||
Process.sleep(50)
|
||||
|
||||
# No checks should exist
|
||||
checks = Monitoring.list_devices_checks(device.id)
|
||||
assert checks == []
|
||||
|
||||
# Cleanup
|
||||
GenServer.stop(pid)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue