towerops/test/towerops/workers/poll_worker_test.exs
Graham McIntire dc7db8ce39
Fix all remaining worker test failures
- 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)
2026-01-20 12:20:04 -06:00

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