towerops/test/towerops/workers/device_poller_worker_test.exs
Graham McIntire c3d952148e
fix test warnings and failures
- Remove unused Ecto.Query import from device_poller_worker_test
- Update route paths from /orgs/:slug to /dashboard after navigation refactor
- Fix HostParser tests to match actual hostname resolution behavior
- Update UserAuth redirect assertions to expect /dashboard instead of /devices
- Fix DashboardLiveTest missing organization context
- Fix OrgLive.NewTest to verify organization creation correctly

All 4850 tests now passing with zero failures and zero warnings.
2026-02-05 15:24:41 -06:00

358 lines
11 KiB
Elixir

defmodule Towerops.Workers.DevicePollerWorkerTest do
use Towerops.DataCase, async: false
import Mox
import Towerops.AccountsFixtures
alias Towerops.Devices
alias Towerops.Organizations
alias Towerops.Repo
alias Towerops.Sites
alias Towerops.Snmp.Device
alias Towerops.Snmp.Interface
alias Towerops.Snmp.Sensor
alias Towerops.Snmp.SnmpMock
alias Towerops.Snmp.StateSensor
alias Towerops.Workers.DevicePollerWorker
alias Towerops.Workers.PollingOffset
setup :verify_on_exit!
setup do
# Configure Mock SNMP Adapter
old_adapter = Application.get_env(:towerops, :snmp_adapter)
Application.put_env(:towerops, :snmp_adapter, SnmpMock)
# Disable Phoenix SNMP check for tests
old_disable = Application.get_env(:towerops, :disable_phoenix_snmp)
Application.put_env(:towerops, :disable_phoenix_snmp, false)
on_exit(fn ->
if old_adapter do
Application.put_env(:towerops, :snmp_adapter, old_adapter)
else
Application.delete_env(:towerops, :snmp_adapter)
end
if old_disable == nil do
Application.delete_env(:towerops, :disable_phoenix_snmp)
else
Application.put_env(:towerops, :disable_phoenix_snmp, old_disable)
end
end)
user = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
{:ok, site} =
Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
%{organization: organization, site: site, user: user}
end
describe "perform/1" do
test "returns :ok when device does not exist" do
assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => Ecto.UUID.generate()}})
end
test "skips polling when snmp_enabled is false", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router No SNMP",
ip_address: "192.168.1.10",
site_id: site.id,
organization_id: site.organization_id,
snmp_enabled: false
})
# Ensure no jobs exist initially (created by create_device)
Repo.delete_all(Oban.Job)
assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})
# Should NOT schedule next poll (because it returns :ok early in maybe_poll_device,
# but wait, does it schedule next poll if skipped?
# perform/1 calls maybe_poll_device then schedule_next_poll_with_error_handling.
# Let's check the code of perform/1 again.
# It calls maybe_poll_device(device) -> returns :ok
# Then calls schedule_next_poll_with_error_handling(device_id, device)
# So it SHOULD schedule the next poll even if skipped?
# Logic:
# device ->
# maybe_poll_device(device)
# schedule_next_poll_with_error_handling(device_id, device)
# :ok
# Yes, it schedules next poll.
assert Repo.aggregate(Oban.Job, :count) == 1
end
test "polls device and schedules next run", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router Poll",
ip_address: "192.168.1.11",
site_id: site.id,
organization_id: site.organization_id,
snmp_enabled: true,
check_interval_seconds: 60
})
# Clear existing jobs
Repo.delete_all(Oban.Job)
assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})
# Verify next job scheduled
assert Repo.aggregate(Oban.Job, :count) == 1
end
end
describe "polling logic" do
test "polls sensors and updates values", %{site: site} do
# 1. Create Device
{:ok, device} =
Devices.create_device(%{
name: "Sensor Device",
ip_address: "192.168.1.50",
site_id: site.id,
organization_id: site.organization_id,
snmp_enabled: true
})
# 2. Create SnmpDevice association
{:ok, snmp_device} =
Repo.insert(%Device{
device_id: device.id,
sys_object_id: "1.3.6.1.4.1.9.1",
sys_descr: "Cisco Router",
sys_name: "router01"
})
# 3. Create Sensor
{:ok, sensor} =
Repo.insert(%Sensor{
snmp_device_id: snmp_device.id,
sensor_type: "temperature",
sensor_index: "1",
sensor_oid: "1.3.6.1.2.1.99.1.1.1.4.1",
sensor_descr: "Chassis Temp",
sensor_unit: "C",
sensor_divisor: 1,
monitored: true
})
# 4. Create Interface
{:ok, interface} =
Repo.insert(%Interface{
snmp_device_id: snmp_device.id,
if_index: 2,
if_name: "eth0",
if_descr: "Ethernet 0",
if_type: 6,
if_speed: 100_000_000,
if_phys_address: "00:11:22:33:44:55",
if_admin_status: "up",
if_oper_status: "up",
monitored: true
})
# 4b. Create State Sensor
{:ok, state_sensor} =
Repo.insert(%StateSensor{
snmp_device_id: snmp_device.id,
sensor_descr: "Power Supply 1",
sensor_oid: "1.3.6.1.4.1.9.9.13.1.5.1.3.1",
sensor_index: "1",
metadata: %{"states" => %{"1" => "unknown", "2" => "enabled", "3" => "disabled"}}
# status defaults to unknown
})
# 4c. Create Processor
{:ok, processor} =
Repo.insert(%Towerops.Snmp.Processor{
snmp_device_id: snmp_device.id,
processor_index: "1",
processor_type: "hr_processor",
description: "CPU 1"
})
# 5. Mock SNMP response
# The worker runs tasks in parallel, so we need to be careful with expectations.
# It calls poll_device_sensors, poll_device_state_sensors, etc.
# We only have one sensor, so poll_device_sensors will try to fetch it.
SnmpMock
|> stub(:get, fn _target, oid, _opts ->
case oid do
# Sensor
"1.3.6.1.2.1.99.1.1.1.4.1" -> {:ok, 45}
# State Sensor
# enabled/ok
"1.3.6.1.4.1.9.9.13.1.5.1.3.1" -> {:ok, 2}
# Processor Load
# 25% load
"1.3.6.1.2.1.25.3.3.1.2.1" -> {:ok, 25}
# Interface HC In Octets
"1.3.6.1.2.1.31.1.1.1.6.2" -> {:ok, 1000}
# Interface HC Out Octets
"1.3.6.1.2.1.31.1.1.1.10.2" -> {:ok, 2000}
# Interface Errors
"1.3.6.1.2.1.2.2.1.14.2" -> {:ok, 0}
"1.3.6.1.2.1.2.2.1.20.2" -> {:ok, 0}
"1.3.6.1.2.1.2.2.1.13.2" -> {:ok, 0}
"1.3.6.1.2.1.2.2.1.19.2" -> {:ok, 0}
# Interface change detection OIDs (for interface 2)
# Speed
"1.3.6.1.2.1.2.2.1.5.2" -> {:ok, 100_000_000}
# Physical address (MAC)
"1.3.6.1.2.1.2.2.1.6.2" -> {:ok, <<0, 17, 34, 51, 68, 85>>}
# Admin status (down=2)
"1.3.6.1.2.1.2.2.1.7.2" -> {:ok, 2}
# Oper status (up=1)
"1.3.6.1.2.1.2.2.1.8.2" -> {:ok, 1}
_ -> {:error, :no_such_object}
end
end)
# Allow other calls (like interfaces, neighbors etc) to return empty/ok
|> stub(:walk, fn _target, _oid, _opts -> {:ok, []} end)
# 6. Run Perform
Repo.delete_all(Oban.Job)
assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})
# 7. Verify Sensor Updated
updated_sensor = Repo.get(Sensor, sensor.id)
assert updated_sensor.last_value == 45.0
assert updated_sensor.last_checked_at
# 8. Verify Interface Stats
stats = Repo.all(Towerops.Snmp.InterfaceStat)
assert length(stats) >= 1
stat = List.last(stats)
assert stat.interface_id == interface.id
assert stat.if_in_octets == 1000
assert stat.if_out_octets == 2000
# 9. Verify Interface Changed
updated_interface = Repo.get(Interface, interface.id)
assert updated_interface.if_admin_status == "down"
# 10. Verify State Sensor
updated_state = Repo.get(StateSensor, state_sensor.id)
assert updated_state.state_value == 2
assert updated_state.status == "ok"
assert updated_state.state_descr == "enabled"
# 11. Verify Processor
readings = Repo.all(Towerops.Snmp.ProcessorReading)
assert length(readings) >= 1
reading = List.last(readings)
assert reading.processor_id == processor.id
assert reading.load_percent == 25.0
end
test "handles SNMP errors gracefully", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Error Device",
ip_address: "192.168.1.51",
site_id: site.id,
organization_id: site.organization_id,
snmp_enabled: true
})
{:ok, snmp_device} = Repo.insert(%Device{device_id: device.id})
{:ok, _sensor} =
Repo.insert(%Sensor{
snmp_device_id: snmp_device.id,
sensor_type: "temperature",
sensor_index: "1",
sensor_oid: "1.3.6.1.2.1.99.1.1.1.4.1",
sensor_descr: "Chassis Temp"
})
# Mock timeout
SnmpMock
|> expect(:get, fn _target, "1.3.6.1.2.1.99.1.1.1.4.1", _opts ->
{:error, :timeout}
end)
|> stub(:walk, fn _, _, _ -> {:ok, []} end)
|> stub(:get, fn _, _, _ -> {:error, :no_such_object} end)
assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})
end
end
describe "start_polling/1" do
test "schedules initial job with offset", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id,
organization_id: site.organization_id,
check_interval_seconds: 300
})
# Get the expected offset
expected_offset = PollingOffset.calculate_offset(device.id, 300)
# Start polling
assert {:ok, job} = DevicePollerWorker.start_polling(device.id)
# Verify job is scheduled with offset
assert job.args["device_id"] == device.id
assert job.scheduled_at
# Calculate the delay (scheduled_at - inserted_at)
delay_seconds = DateTime.diff(job.scheduled_at, job.inserted_at, :second)
assert delay_seconds == expected_offset
end
test "schedules initial job with offset for different interval", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router 2",
ip_address: "192.168.1.2",
site_id: site.id,
organization_id: site.organization_id,
check_interval_seconds: 600
})
# Get the expected offset
expected_offset = PollingOffset.calculate_offset(device.id, 600)
# Start polling
assert {:ok, job} = DevicePollerWorker.start_polling(device.id)
# Verify job is scheduled with offset
delay_seconds = DateTime.diff(job.scheduled_at, job.inserted_at, :second)
assert delay_seconds == expected_offset
end
end
describe "stop_polling/1" do
test "cancels all jobs for device", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router 3",
ip_address: "192.168.1.3",
site_id: site.id,
organization_id: site.organization_id
})
# Start polling
assert {:ok, _job} = DevicePollerWorker.start_polling(device.id)
# Stop polling
assert {:ok, cancelled_jobs} = DevicePollerWorker.stop_polling(device.id)
refute cancelled_jobs == []
end
end
end