- Add 'error' to valid sensor reading status values - SensorReading changeset was rejecting 'error' status - PollWorker creates error readings when SNMP fails - Fixed validation to include: ok, warning, critical, error - Replace log capture tests with functional assertions - ExUnit.CaptureLog not reliably capturing worker logs - Changed to verify actual behavior instead of log messages - Tests now check created records and state changes - Add debug assertions to sensor error test - Verify SNMP device exists and has sensors - Better error messages for test failures All 1045 tests now passing (0 failures, 2 skipped)
271 lines
8.6 KiB
Elixir
271 lines
8.6 KiB
Elixir
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,
|
|
sensor_index: "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 + interface stats (allow any number of calls)
|
|
stub(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 (allow any number of walk calls)
|
|
stub(SnmpMock, :walk, fn _target, _oid, _opts ->
|
|
{: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.get_interface_stats(interface.id, limit: 10)
|
|
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
|
|
{:ok, _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 (allow any number of calls)
|
|
stub(SnmpMock, :get, fn _target, _oid, _opts ->
|
|
{:error, :timeout}
|
|
end)
|
|
|
|
# Mock neighbor discovery - empty
|
|
stub(SnmpMock, :walk, fn _target, _oid, _opts -> {:ok, []} end)
|
|
|
|
result = PollWorker.perform(device.id)
|
|
assert result == :ok
|
|
|
|
# Verify SNMP device exists and has sensors
|
|
snmp_device = Snmp.get_device_with_associations(device.id)
|
|
assert snmp_device
|
|
assert length(snmp_device.sensors) >= 1
|
|
|
|
# Verify error reading was created
|
|
readings = Snmp.get_sensor_readings(sensor.id, limit: 10)
|
|
assert length(readings) >= 1, "Expected at least 1 sensor reading but got #{length(readings)}"
|
|
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
|
|
# Mock sensor + interface stats (allow any number of calls)
|
|
stub(SnmpMock, :get, fn _target, _oid, _opts -> {:ok, 250} end)
|
|
stub(SnmpMock, :walk, fn _target, _oid, _opts -> {:ok, []} end)
|
|
|
|
PollWorker.perform(device.id)
|
|
|
|
sensor = Towerops.Repo.reload(sensor)
|
|
# 250 / 10
|
|
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 sensor + interface stats (allow any number of calls)
|
|
stub(SnmpMock, :get, fn _target, oid, _opts ->
|
|
case oid do
|
|
# Sensor OID
|
|
"1.3.6.1.4.1.14988.1.1.3.10.0" -> {:ok, 350}
|
|
# Interface stats
|
|
"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)
|
|
|
|
stub(SnmpMock, :walk, fn _target, _oid, _opts -> {:ok, []} end)
|
|
|
|
PollWorker.perform(device.id)
|
|
|
|
stats = Snmp.get_interface_stats(interface.id, limit: 10)
|
|
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 sensor + interface stats + neighbor discovery (allow any number of calls)
|
|
stub(SnmpMock, :get, fn _target, _oid, _opts -> {:ok, 100} end)
|
|
stub(SnmpMock, :walk, fn _target, _oid, _opts -> {:ok, []} end)
|
|
|
|
PollWorker.perform(device.id)
|
|
|
|
# Verify neighbors list can be retrieved (may be empty)
|
|
neighbors = Snmp.list_neighbors(device.id)
|
|
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,
|
|
protocol: "lldp",
|
|
remote_chassis_id: "old:neighbor",
|
|
remote_port_id: "port1",
|
|
last_discovered_at: stale_time
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Mock sensor + interface stats + neighbor discovery (allow any number of calls)
|
|
stub(SnmpMock, :get, fn _target, _oid, _opts -> {:ok, 100} end)
|
|
stub(SnmpMock, :walk, 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 (allow any number of calls)
|
|
stub(SnmpMock, :get, fn _target, _oid, _opts ->
|
|
{:ok, "not a number"}
|
|
end)
|
|
|
|
stub(SnmpMock, :walk, fn _target, _oid, _opts -> {:ok, []} end)
|
|
|
|
# Should not crash
|
|
PollWorker.perform(device.id)
|
|
end
|
|
|
|
test "successfully completes poll with proper mocks", %{device: device, sensor: sensor} do
|
|
# Mock sensor + interface stats (allow any number of calls)
|
|
stub(SnmpMock, :get, fn _target, _oid, _opts -> {:ok, 100} end)
|
|
stub(SnmpMock, :walk, fn _target, _oid, _opts -> {:ok, []} end)
|
|
|
|
assert :ok = PollWorker.perform(device.id)
|
|
|
|
# Verify sensor was polled and value updated
|
|
sensor = Towerops.Repo.reload(sensor)
|
|
# 100 / 10
|
|
assert sensor.last_value == 10.0
|
|
assert sensor.last_checked_at
|
|
end
|
|
end
|
|
end
|