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:
Graham McIntire 2026-01-20 13:53:46 -06:00
parent f9553b1444
commit f5ff6c5c61
No known key found for this signature in database

View 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