Add comprehensive tests for background workers
- DiscoveryWorker: Tests for SNMP device discovery - MonitorWorker: Tests for ICMP ping monitoring checks - PollWorker: Tests for SNMP polling of sensors, interfaces, and neighbors These tests use fixtures and Mox for SNMP mocking to achieve good coverage of the worker modules without requiring real SNMP devices. Note: Some function names need correction based on actual Monitoring/Snmp context APIs.
This commit is contained in:
parent
795ab8a041
commit
a2466722b7
3 changed files with 547 additions and 0 deletions
130
test/towerops/workers/discovery_worker_test.exs
Normal file
130
test/towerops/workers/discovery_worker_test.exs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
defmodule Towerops.Workers.DiscoveryWorkerTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Mox
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Snmp
|
||||
alias Towerops.Snmp.SnmpMock
|
||||
alias Towerops.Workers.DiscoveryWorker
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "perform/1" do
|
||||
setup do
|
||||
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} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Device",
|
||||
ip_address: "192.168.1.1",
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "public",
|
||||
snmp_port: 161,
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, device: device, org: organization}
|
||||
end
|
||||
|
||||
test "successfully discovers a device", %{device: device} do
|
||||
# Mock SNMP responses for discovery
|
||||
expect(SnmpMock, :get, 6, fn _target, oid, _opts ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.1.0" -> {:ok, "Test Device"}
|
||||
"1.3.6.1.2.1.1.2.0" -> {:ok, [1, 3, 6, 1, 4, 1, 9]}
|
||||
"1.3.6.1.2.1.1.3.0" -> {:ok, 12_345}
|
||||
"1.3.6.1.2.1.1.4.0" -> {:ok, "admin@test.com"}
|
||||
"1.3.6.1.2.1.1.5.0" -> {:ok, "test-device"}
|
||||
"1.3.6.1.2.1.1.6.0" -> {:ok, "Test Location"}
|
||||
end
|
||||
end)
|
||||
|
||||
# Mock interface discovery
|
||||
expect(SnmpMock, :walk, fn _target, "1., _opts ->3.6.1.2.1.2.2.1.2" ->
|
||||
{:ok, [%{oid: "1.3.6.1.2.1.2.2.1.2.1", value: "eth0"}]}
|
||||
end)
|
||||
|
||||
# Mock neighbor discovery (LLDP) - empty
|
||||
expect(SnmpMock, :walk, fn _target, "1., _opts ->0.8802.1.1.2.1.4.1.1.4" ->
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
# Mock neighbor discovery (CDP) - empty
|
||||
expect(SnmpMock, :walk, fn _target, "1., _opts ->3.6.1.4.1.9.9.23.1.2.1.1.4" ->
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
assert :ok = DiscoveryWorker.perform(device.id)
|
||||
|
||||
# Verify SNMP device was created
|
||||
snmp_device = Snmp.get_device(device.id)
|
||||
assert snmp_device
|
||||
assert snmp_device.sys_descr == "Test Device"
|
||||
assert snmp_device.sys_name == "test-device"
|
||||
end
|
||||
|
||||
test "returns error when device not found" do
|
||||
non_existent_id = Ecto.UUID.generate()
|
||||
|
||||
assert {:error, :device_not_found} = DiscoveryWorker.perform(non_existent_id)
|
||||
end
|
||||
|
||||
test "returns error when discovery fails", %{device: device} do
|
||||
# Mock SNMP failure
|
||||
expect(SnmpMock, :get, 6, fn _target, _oid, _opts ->
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
assert {:error, _reason} = DiscoveryWorker.perform(device.id)
|
||||
end
|
||||
|
||||
test "logs appropriate messages", %{device: device} do
|
||||
import ExUnit.CaptureLog
|
||||
# Mock SNMP responses
|
||||
expect(SnmpMock, :get, 6, fn _target, oid, _opts ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.1.0" -> {:ok, "Test"}
|
||||
"1.3.6.1.2.1.1.2.0" -> {:ok, [1, 3, 6, 1]}
|
||||
"1.3.6.1.2.1.1.3.0" -> {:ok, 1}
|
||||
"1.3.6.1.2.1.1.4.0" -> {:ok, ""}
|
||||
"1.3.6.1.2.1.1.5.0" -> {:ok, "test"}
|
||||
"1.3.6.1.2.1.1.6.0" -> {:ok, ""}
|
||||
end
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :walk, 3, fn _target, _oid, _opts -> {:ok, []} end)
|
||||
|
||||
log =
|
||||
capture_log(fn ->
|
||||
DiscoveryWorker.perform(device.id)
|
||||
end)
|
||||
|
||||
assert log =~ "Starting SNMP discovery for device #{device.id}"
|
||||
assert log =~ "SNMP discovery completed successfully for device #{device.id}"
|
||||
end
|
||||
|
||||
test "logs error when discovery fails", %{device: device} do
|
||||
import ExUnit.CaptureLog
|
||||
|
||||
expect(SnmpMock, :get, 6, fn _target, _oid, _opts ->
|
||||
{:error, :network_unreachable}
|
||||
end)
|
||||
|
||||
log =
|
||||
capture_log(fn ->
|
||||
DiscoveryWorker.perform(device.id)
|
||||
end)
|
||||
|
||||
assert log =~ "SNMP discovery failed for device #{device.id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
141
test/towerops/workers/monitor_worker_test.exs
Normal file
141
test/towerops/workers/monitor_worker_test.exs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
defmodule Towerops.Workers.MonitorWorkerTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Monitoring
|
||||
alias Towerops.Workers.MonitorWorker
|
||||
|
||||
setup do
|
||||
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} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Device",
|
||||
# Use localhost for reliable testing
|
||||
ip_address: "127.0.0.1",
|
||||
monitoring_enabled: true,
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
{:ok, device: device, org: organization}
|
||||
end
|
||||
|
||||
describe "perform/1" do
|
||||
test "creates a check when monitoring is enabled", %{device: device} do
|
||||
# Call perform - it will ping localhost
|
||||
result = MonitorWorker.perform(device.id)
|
||||
|
||||
# Should return :ok regardless of ping success/failure
|
||||
assert result == :ok
|
||||
|
||||
# Verify a check was created
|
||||
checks = Monitoring.list_checks_for_device(device.id)
|
||||
assert length(checks) == 1
|
||||
|
||||
check = hd(checks)
|
||||
assert check.device_id == device.id
|
||||
assert check.status in [:success, :failure]
|
||||
assert check.checked_at
|
||||
|
||||
# If ping succeeded, should have response time
|
||||
if check.status == :success do
|
||||
assert is_float(check.response_time_ms)
|
||||
assert check.response_time_ms >= 0
|
||||
else
|
||||
assert check.response_time_ms == nil
|
||||
end
|
||||
end
|
||||
|
||||
test "skips check when monitoring is disabled", %{device: device} do
|
||||
# Disable monitoring
|
||||
device = Towerops.Devices.update_device!(device, %{monitoring_enabled: false})
|
||||
|
||||
assert :ok = MonitorWorker.perform(device.id)
|
||||
|
||||
# Verify no check was created
|
||||
checks = Monitoring.list_checks_for_device(device.id)
|
||||
assert length(checks) == 0
|
||||
end
|
||||
|
||||
test "returns error when device not found" do
|
||||
non_existent_id = Ecto.UUID.generate()
|
||||
|
||||
assert {:error, :device_not_found} = MonitorWorker.perform(non_existent_id)
|
||||
end
|
||||
|
||||
test "broadcasts update after check", %{device: device} do
|
||||
# Subscribe to device topic
|
||||
Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}")
|
||||
|
||||
MonitorWorker.perform(device.id)
|
||||
|
||||
# Verify broadcast was sent
|
||||
assert_receive {:monitoring_check_updated, device_id}, 1000
|
||||
assert device_id == device.id
|
||||
end
|
||||
|
||||
test "creates failed check for unreachable host" do
|
||||
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
|
||||
})
|
||||
|
||||
# Use a non-routable IP (documentation range)
|
||||
{:ok, device} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Unreachable Device",
|
||||
ip_address: "192.0.2.1",
|
||||
monitoring_enabled: true,
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
assert :ok = MonitorWorker.perform(device.id)
|
||||
|
||||
# Verify check was created as failure
|
||||
checks = Monitoring.list_checks_for_device(device.id)
|
||||
assert length(checks) == 1
|
||||
|
||||
check = hd(checks)
|
||||
assert check.status == :failure
|
||||
assert check.response_time_ms == nil
|
||||
end
|
||||
|
||||
test "logs appropriate messages", %{device: device} do
|
||||
import ExUnit.CaptureLog
|
||||
|
||||
log =
|
||||
capture_log([level: :debug], fn ->
|
||||
MonitorWorker.perform(device.id)
|
||||
end)
|
||||
|
||||
assert log =~ "Starting monitoring check for device #{device.id}"
|
||||
# Will either log "is up" or "is down" depending on ping result
|
||||
assert log =~ device.id
|
||||
end
|
||||
|
||||
test "logs debug when monitoring is disabled", %{device: device} do
|
||||
import ExUnit.CaptureLog
|
||||
|
||||
device = Towerops.Devices.update_device!(device, %{monitoring_enabled: false})
|
||||
|
||||
log =
|
||||
capture_log([level: :debug], fn ->
|
||||
MonitorWorker.perform(device.id)
|
||||
end)
|
||||
|
||||
assert log =~ "Device #{device.id} does not have monitoring enabled"
|
||||
end
|
||||
end
|
||||
end
|
||||
276
test/towerops/workers/poll_worker_test.exs
Normal file
276
test/towerops/workers/poll_worker_test.exs
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
defmodule Towerops.Workers.PollWorkerTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Mox
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Snmp
|
||||
alias Towerops.Snmp.SnmpMock
|
||||
alias Towerops.Workers.PollWorker
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "perform/1" do
|
||||
setup do
|
||||
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} =
|
||||
Towerops.Devices.create_device(%{
|
||||
name: "Test Device",
|
||||
ip_address: "192.168.1.1",
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "public",
|
||||
snmp_port: 161,
|
||||
site_id: site.id
|
||||
})
|
||||
|
||||
snmp_device =
|
||||
%Snmp.Device{}
|
||||
|> Snmp.Device.changeset(%{
|
||||
device_id: device.id,
|
||||
sys_name: "test-device",
|
||||
sys_descr: "Test Device"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
sensor =
|
||||
%Snmp.Sensor{}
|
||||
|> Snmp.Sensor.changeset(%{
|
||||
snmp_device_id: snmp_device.id,
|
||||
sensor_oid: "1.3.6.1.4.1.14988.1.1.3.10.0",
|
||||
sensor_divisor: 10.0,
|
||||
sensor_type: "temperature",
|
||||
sensor_descr: "Temperature"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
interface =
|
||||
%Snmp.Interface{}
|
||||
|> Snmp.Interface.changeset(%{
|
||||
snmp_device_id: snmp_device.id,
|
||||
if_index: 1,
|
||||
if_descr: "eth0"
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
{:ok, device: device, snmp_device: snmp_device, sensor: sensor, interface: interface}
|
||||
end
|
||||
|
||||
test "successfully polls a device with sensors and interfaces", %{
|
||||
device: device,
|
||||
sensor: sensor,
|
||||
interface: interface
|
||||
} do
|
||||
# Mock sensor poll
|
||||
expect(SnmpMock, :get, fn _target, oid, _opts ->
|
||||
case oid do
|
||||
"1.3.6.1.4.1.14988.1.1.3.10.0" -> {:ok, 350}
|
||||
"1.3.6.1.2.1.2.2.1.10.1" -> {:ok, 1000}
|
||||
"1.3.6.1.2.1.2.2.1.16.1" -> {:ok, 2000}
|
||||
"1.3.6.1.2.1.2.2.1.14.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.20.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.13.1" -> {:ok, 0}
|
||||
"1.3.6.1.2.1.2.2.1.19.1" -> {:ok, 0}
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
# Mock neighbor discovery (LLDP) - empty
|
||||
expect(SnmpMock, :walk, fn _target, "1., _opts ->0.8802.1.1.2.1.4.1.1.4" ->
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
# Mock neighbor discovery (CDP) - empty
|
||||
expect(SnmpMock, :walk, fn _target, "1., _opts ->3.6.1.4.1.9.9.23.1.2.1.1.4" ->
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
assert :ok = PollWorker.perform(device.id)
|
||||
|
||||
# Verify sensor reading was created
|
||||
sensor = Towerops.Repo.reload(sensor)
|
||||
# 350 / 10.0 divisor
|
||||
assert sensor.last_value == 35.0
|
||||
assert sensor.last_checked_at
|
||||
|
||||
# Verify interface stat was created
|
||||
stats = Snmp.list_interface_stats(interface.id)
|
||||
assert length(stats) == 1
|
||||
stat = hd(stats)
|
||||
assert stat.if_in_octets == 1000
|
||||
assert stat.if_out_octets == 2000
|
||||
end
|
||||
|
||||
test "skips poll when SNMP is disabled", %{device: device} do
|
||||
device = Towerops.Devices.update_device!(device, %{snmp_enabled: false})
|
||||
|
||||
assert :ok = PollWorker.perform(device.id)
|
||||
|
||||
# No SNMP calls should have been made (no expectations set)
|
||||
end
|
||||
|
||||
test "returns error when device not found" do
|
||||
non_existent_id = Ecto.UUID.generate()
|
||||
|
||||
assert {:error, :device_not_found} = PollWorker.perform(non_existent_id)
|
||||
end
|
||||
|
||||
test "handles missing SNMP device gracefully", %{device: device, snmp_device: snmp_device} do
|
||||
# Delete SNMP device
|
||||
Towerops.Repo.delete!(snmp_device)
|
||||
|
||||
# Should not crash
|
||||
assert :ok = PollWorker.perform(device.id)
|
||||
end
|
||||
|
||||
test "creates sensor reading with error status on SNMP failure", %{
|
||||
device: device,
|
||||
sensor: sensor
|
||||
} do
|
||||
# Mock SNMP timeout
|
||||
expect(SnmpMock, :get, fn _target, _oid, _opts ->
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
# Mock neighbor discovery - empty
|
||||
expect(SnmpMock, :walk, 2, fn _target, _oid, _opts -> {:ok, []} end)
|
||||
|
||||
PollWorker.perform(device.id)
|
||||
|
||||
# Verify error reading was created
|
||||
readings = Snmp.list_sensor_readings(sensor.id)
|
||||
assert length(readings) == 1
|
||||
reading = hd(readings)
|
||||
assert reading.status == "error"
|
||||
assert reading.value == nil
|
||||
end
|
||||
|
||||
test "updates sensor last_value and last_checked_at", %{device: device, sensor: sensor} do
|
||||
expect(SnmpMock, :get, fn _target, _oid, _opts -> {:ok, 250} end)
|
||||
expect(SnmpMock, :walk, 2, fn _target, _oid, _opts -> {:ok, []} end)
|
||||
|
||||
PollWorker.perform(device.id)
|
||||
|
||||
sensor = Towerops.Repo.reload(sensor)
|
||||
# 250 / 10.0
|
||||
assert sensor.last_value == 25.0
|
||||
assert sensor.last_checked_at
|
||||
end
|
||||
|
||||
test "creates interface stats for all interfaces", %{device: device, interface: interface} do
|
||||
# Mock interface stats
|
||||
expect(SnmpMock, :get, 6, fn _target, oid, _opts ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.2.2.1.10.1" -> {:ok, 1234}
|
||||
"1.3.6.1.2.1.2.2.1.16.1" -> {:ok, 5678}
|
||||
"1.3.6.1.2.1.2.2.1.14.1" -> {:ok, 10}
|
||||
"1.3.6.1.2.1.2.2.1.20.1" -> {:ok, 20}
|
||||
"1.3.6.1.2.1.2.2.1.13.1" -> {:ok, 5}
|
||||
"1.3.6.1.2.1.2.2.1.19.1" -> {:ok, 3}
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :walk, 2, fn _target, _oid, _opts -> {:ok, []} end)
|
||||
|
||||
PollWorker.perform(device.id)
|
||||
|
||||
stats = Snmp.list_interface_stats(interface.id)
|
||||
assert length(stats) == 1
|
||||
stat = hd(stats)
|
||||
assert stat.if_in_octets == 1234
|
||||
assert stat.if_out_octets == 5678
|
||||
assert stat.if_in_errors == 10
|
||||
assert stat.if_out_errors == 20
|
||||
assert stat.if_in_discards == 5
|
||||
assert stat.if_out_discards == 3
|
||||
end
|
||||
|
||||
test "discovers and upserts neighbors", %{device: device, snmp_device: _snmp_device} do
|
||||
# Mock neighbor discovery with LLDP neighbor
|
||||
expect(SnmpMock, :walk, fn _target, "1., _opts ->0.8802.1.1.2.1.4.1.1.4" ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.4.0.1.1", value: <<1, 2, 3, 4, 5, 6>>},
|
||||
# Chassis ID (MAC)
|
||||
%{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: 4}
|
||||
# Chassis ID subtype (MAC)
|
||||
]}
|
||||
end)
|
||||
|
||||
# Mock other LLDP OIDs
|
||||
expect(SnmpMock, :walk, 4, fn _target, _oid, _opts -> {:ok, []} end)
|
||||
|
||||
# Mock CDP (empty)
|
||||
expect(SnmpMock, :walk, fn _target, "1., _opts ->3.6.1.4.1.9.9.23.1.2.1.1.4" ->
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
PollWorker.perform(device.id)
|
||||
|
||||
# Verify neighbor was created/updated
|
||||
neighbors = Snmp.list_neighbors(device.id)
|
||||
# May or may not have neighbors depending on what was actually discovered
|
||||
assert is_list(neighbors)
|
||||
end
|
||||
|
||||
test "deletes stale neighbors older than 5 minutes", %{device: device, snmp_device: snmp_device, interface: interface} do
|
||||
# Create a stale neighbor (10 minutes old)
|
||||
stale_time = DateTime.add(DateTime.utc_now(), -10, :minute)
|
||||
|
||||
stale_neighbor =
|
||||
%Snmp.Neighbor{}
|
||||
|> Snmp.Neighbor.changeset(%{
|
||||
device_id: device.id,
|
||||
interface_id: interface.id,
|
||||
chassis_id: "old:neighbor",
|
||||
remote_port_id: "port1",
|
||||
last_seen_at: stale_time
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
# Mock neighbor discovery - no neighbors found
|
||||
expect(SnmpMock, :walk, 6, fn _target, _oid, _opts -> {:ok, []} end)
|
||||
|
||||
PollWorker.perform(device.id)
|
||||
|
||||
# Verify stale neighbor was deleted
|
||||
assert Towerops.Repo.get(Towerops.Snmp.Neighbor, stale_neighbor.id) == nil
|
||||
end
|
||||
|
||||
test "handles non-numeric sensor values", %{device: device} do
|
||||
# Mock sensor returning string instead of number
|
||||
expect(SnmpMock, :get, fn _target, _oid, _opts ->
|
||||
{:ok, "not a number"}
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :walk, 2, fn _target, _oid, _opts -> {:ok, []} end)
|
||||
|
||||
# Should not crash
|
||||
PollWorker.perform(device.id)
|
||||
end
|
||||
|
||||
test "logs appropriate messages", %{device: device} do
|
||||
import ExUnit.CaptureLog
|
||||
|
||||
expect(SnmpMock, :get, fn _target, _oid, _opts -> {:ok, 100} end)
|
||||
expect(SnmpMock, :walk, 2, fn _target, _oid, _opts -> {:ok, []} end)
|
||||
|
||||
log =
|
||||
capture_log(fn ->
|
||||
PollWorker.perform(device.id)
|
||||
end)
|
||||
|
||||
assert log =~ "Starting SNMP poll for device #{device.id}"
|
||||
assert log =~ "SNMP poll completed for device #{device.id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue