towerops/test/towerops/devices_test.exs

1297 lines
41 KiB
Elixir

defmodule Towerops.EquipmentTest do
use Towerops.DataCase
use ExUnitProperties
alias Towerops.Devices
alias Towerops.Devices.Device
describe "device" do
import Towerops.AccountsFixtures
alias Device, as: DeviceSchema
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
})
%{organization: organization, site: site, user: user}
end
@valid_attrs %{
name: "Router 1",
ip_address: "192.168.1.1",
description: "Main router",
monitoring_enabled: true,
check_interval_seconds: 300
}
@update_attrs %{
name: "Updated Router",
ip_address: "192.168.1.2",
description: "Updated",
monitoring_enabled: false
}
@invalid_attrs %{name: nil, ip_address: nil}
test "list_site_devices/1 returns all devices for a site", %{site: site} do
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
assert Devices.list_site_devices(site.id) == [device]
end
test "list_organization_devices/1 returns all devices for an organization", %{
organization: organization,
site: site
} do
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
result = Devices.list_organization_devices(organization.id)
assert length(result) == 1
assert hd(result).id == device.id
end
test "list_monitored_devices/0 returns only device with monitoring enabled", %{
site: site
} do
{:ok, monitored} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
{:ok, _not_monitored} =
Devices.create_device(%{
name: "Not Monitored",
ip_address: "192.168.1.3",
site_id: site.id,
monitoring_enabled: false
})
result = Devices.list_monitored_devices()
assert length(result) == 1
assert hd(result).id == monitored.id
end
test "get_device!/1 returns the device with given id", %{site: site} do
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
assert Devices.get_device!(device.id).id == device.id
end
test "get_site_equipment!/2 returns device for specific site", %{site: site} do
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
assert Devices.get_site_device!(site.id, device.id).id == device.id
end
test "create_device/1 with valid data creates device", %{site: site} do
attrs = Map.put(@valid_attrs, :site_id, site.id)
assert {:ok, %DeviceSchema{} = device} = Devices.create_device(attrs)
assert device.name == "Router 1"
assert device.ip_address == "192.168.1.1"
assert device.status == :unknown
assert device.monitoring_enabled == true
assert device.check_interval_seconds == 300
end
test "create_device/1 with valid IPv6 address", %{site: site} do
attrs = Map.put(@valid_attrs, :ip_address, "2001:0db8:85a3::8a2e:0370:7334")
attrs = Map.put(attrs, :site_id, site.id)
assert {:ok, %DeviceSchema{}} = Devices.create_device(attrs)
end
test "create_device/1 with invalid IP address returns error", %{site: site} do
attrs = Map.put(@valid_attrs, :ip_address, "invalid-ip")
attrs = Map.put(attrs, :site_id, site.id)
assert {:error, changeset} = Devices.create_device(attrs)
assert "must be a valid IPv4 or IPv6 address" in errors_on(changeset).ip_address
end
test "create_device/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Devices.create_device(@invalid_attrs)
end
test "update_device/2 with valid data updates the device", %{site: site} do
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
assert {:ok, %DeviceSchema{} = device} =
Devices.update_device(device, @update_attrs)
assert device.name == "Updated Router"
assert device.ip_address == "192.168.1.2"
assert device.monitoring_enabled == false
end
test "update_device/2 with invalid data returns error changeset", %{site: site} do
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
assert {:error, %Ecto.Changeset{}} =
Devices.update_device(device, @invalid_attrs)
assert Devices.get_device!(device.id).name == device.name
end
test "change_device/1 returns an device changeset", %{site: site} do
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
assert %Ecto.Changeset{} = Devices.change_device(device)
end
test "update_device_status/2 updates status and timestamps", %{site: site} do
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
assert {:ok, updated} = Devices.update_device_status(device, :up)
assert updated.status == :up
assert updated.last_checked_at
assert updated.last_status_change_at
end
test "update_device_status/2 only updates last_checked_at if status unchanged", %{
site: site
} do
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
{:ok, device} = Devices.update_device_status(device, :up)
# Manually set timestamps to 1 hour ago to avoid sleeping
# Must truncate to :second to match DB column type
past_time =
DateTime.utc_now()
|> DateTime.add(-3600, :second)
|> DateTime.truncate(:second)
{:ok, device} =
device
|> Ecto.Changeset.change(%{last_checked_at: past_time, last_status_change_at: past_time})
|> Towerops.Repo.update()
first_change_at = device.last_status_change_at
first_checked_at = device.last_checked_at
{:ok, updated} = Devices.update_device_status(device, :up)
# Status change timestamp should not change
assert updated.last_status_change_at == first_change_at
# But checked_at should be updated (to now, which is after 1 hour ago)
assert DateTime.after?(updated.last_checked_at, first_checked_at)
end
test "list_snmp_enabled_devices/0 returns only device with SNMP enabled", %{site: site} do
{:ok, snmp_enabled} =
Devices.create_device(%{
name: "SNMP Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_community: "public"
})
{:ok, _snmp_disabled} =
Devices.create_device(%{
name: "Non-SNMP Router",
ip_address: "192.168.1.2",
site_id: site.id,
snmp_enabled: false
})
result = Devices.list_snmp_enabled_devices()
assert length(result) == 1
assert hd(result).id == snmp_enabled.id
end
test "update_snmp_poll_time/1 updates last_snmp_poll_at timestamp", %{site: site} do
{:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
assert device.last_snmp_poll_at == nil
assert {:ok, updated} = Devices.update_snmp_poll_time(device)
assert updated.last_snmp_poll_at
assert DateTime.before?(updated.last_snmp_poll_at, DateTime.utc_now())
end
test "create_device/1 with SNMP enabled and nil community string succeeds", %{site: site} do
attrs = %{
name: "SNMP Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_version: "2c",
snmp_community: nil
}
assert {:ok, %DeviceSchema{} = device} = Devices.create_device(attrs)
assert device.snmp_enabled == true
assert device.snmp_community == nil
end
test "create_device/1 with SNMP enabled and empty community string succeeds", %{site: site} do
attrs = %{
name: "SNMP Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_version: "2c",
snmp_community: ""
}
# Empty string should be converted to nil by the database
assert {:ok, %DeviceSchema{} = device} = Devices.create_device(attrs)
assert device.snmp_enabled == true
# Empty strings are stored as nil in the database
assert device.snmp_community == nil || device.snmp_community == ""
end
test "reorder_device/2 reorders device to first position", %{site: site} do
{:ok, device1} = Devices.create_device(%{name: "Device 1", ip_address: "192.168.1.1", site_id: site.id})
{:ok, device2} = Devices.create_device(%{name: "Device 2", ip_address: "192.168.1.2", site_id: site.id})
{:ok, device3} = Devices.create_device(%{name: "Device 3", ip_address: "192.168.1.3", site_id: site.id})
# Move device3 to first position
{:ok, _updated} = Devices.reorder_device(device3.id, 1)
# Verify order
devices = Devices.list_site_devices(site.id)
assert Enum.at(devices, 0).id == device3.id
assert Enum.at(devices, 0).display_order == 1
assert Enum.at(devices, 1).id == device1.id
assert Enum.at(devices, 1).display_order == 2
assert Enum.at(devices, 2).id == device2.id
assert Enum.at(devices, 2).display_order == 3
end
test "reorder_device/2 reorders device to last position", %{site: site} do
{:ok, device1} = Devices.create_device(%{name: "Device 1", ip_address: "192.168.1.1", site_id: site.id})
{:ok, device2} = Devices.create_device(%{name: "Device 2", ip_address: "192.168.1.2", site_id: site.id})
{:ok, device3} = Devices.create_device(%{name: "Device 3", ip_address: "192.168.1.3", site_id: site.id})
# Move device1 to last position
{:ok, _updated} = Devices.reorder_device(device1.id, 3)
# Verify order
devices = Devices.list_site_devices(site.id)
assert Enum.at(devices, 0).id == device2.id
assert Enum.at(devices, 0).display_order == 1
assert Enum.at(devices, 1).id == device3.id
assert Enum.at(devices, 1).display_order == 2
assert Enum.at(devices, 2).id == device1.id
assert Enum.at(devices, 2).display_order == 3
end
test "reorder_device/2 reorders device to middle position", %{site: site} do
{:ok, device1} = Devices.create_device(%{name: "Device 1", ip_address: "192.168.1.1", site_id: site.id})
{:ok, device2} = Devices.create_device(%{name: "Device 2", ip_address: "192.168.1.2", site_id: site.id})
{:ok, device3} = Devices.create_device(%{name: "Device 3", ip_address: "192.168.1.3", site_id: site.id})
# Move device3 to middle position
{:ok, _updated} = Devices.reorder_device(device3.id, 2)
# Verify order
devices = Devices.list_site_devices(site.id)
assert Enum.at(devices, 0).id == device1.id
assert Enum.at(devices, 0).display_order == 1
assert Enum.at(devices, 1).id == device3.id
assert Enum.at(devices, 1).display_order == 2
assert Enum.at(devices, 2).id == device2.id
assert Enum.at(devices, 2).display_order == 3
end
test "reorder_device/2 maintains continuous numbering", %{site: site} do
{:ok, _device1} = Devices.create_device(%{name: "Device 1", ip_address: "192.168.1.1", site_id: site.id})
{:ok, device2} = Devices.create_device(%{name: "Device 2", ip_address: "192.168.1.2", site_id: site.id})
{:ok, _device3} = Devices.create_device(%{name: "Device 3", ip_address: "192.168.1.3", site_id: site.id})
{:ok, device4} = Devices.create_device(%{name: "Device 4", ip_address: "192.168.1.4", site_id: site.id})
# Reorder multiple times
{:ok, _} = Devices.reorder_device(device2.id, 1)
{:ok, _} = Devices.reorder_device(device4.id, 2)
# Verify all have continuous display_order values
devices = Devices.list_site_devices(site.id)
display_orders = Enum.map(devices, & &1.display_order)
assert display_orders == [1, 2, 3, 4]
end
test "reorder_device/2 only affects devices in same site", %{organization: organization, site: site} do
# Create devices in first site
{:ok, device1} = Devices.create_device(%{name: "Device 1", ip_address: "192.168.1.1", site_id: site.id})
{:ok, device2} = Devices.create_device(%{name: "Device 2", ip_address: "192.168.1.2", site_id: site.id})
# Create second site and device
{:ok, site2} = Towerops.Sites.create_site(%{name: "Site 2", organization_id: organization.id})
{:ok, device3} = Devices.create_device(%{name: "Device 3", ip_address: "192.168.2.1", site_id: site2.id})
# Reorder device in first site
{:ok, _} = Devices.reorder_device(device1.id, 2)
# Verify first site is reordered
site1_devices = Devices.list_site_devices(site.id)
assert Enum.at(site1_devices, 0).id == device2.id
assert Enum.at(site1_devices, 1).id == device1.id
# Verify second site is unchanged (no display_order set)
site2_devices = Devices.list_site_devices(site2.id)
assert length(site2_devices) == 1
assert hd(site2_devices).id == device3.id
assert is_nil(hd(site2_devices).display_order)
end
test "reset_organization_device_order/1 clears all display_order values", %{
organization: organization,
site: site
} do
{:ok, device1} = Devices.create_device(%{name: "Zebra Device", ip_address: "192.168.1.1", site_id: site.id})
{:ok, device2} = Devices.create_device(%{name: "Alpha Device", ip_address: "192.168.1.2", site_id: site.id})
# Set custom order
{:ok, _} = Devices.reorder_device(device1.id, 1)
{:ok, _} = Devices.reorder_device(device2.id, 2)
# Verify custom order is set
devices_before = Devices.list_site_devices(site.id)
assert List.first(devices_before).id == device1.id
assert List.first(devices_before).display_order == 1
# Reset order
{count, _} = Devices.reset_organization_device_order(organization.id)
assert count == 2
# Verify display_order is cleared and falls back to alphabetical
devices_after = Devices.list_site_devices(site.id)
assert List.first(devices_after).name == "Alpha Device"
assert List.first(devices_after).display_order == nil
assert List.last(devices_after).name == "Zebra Device"
assert List.last(devices_after).display_order == nil
end
end
describe "SNMP configuration inheritance" do
import Towerops.AccountsFixtures
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
})
%{organization: organization, site: site, user: user}
end
test "get_snmp_config/1 returns device-level community string when set", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_community: "device-community"
})
config = Devices.get_snmp_config(device)
assert config.community == "device-community"
assert config.source == :device
end
test "get_snmp_config/1 returns site-level community when device has nil", %{
organization: _organization,
site: site
} do
# Update site with SNMP community
{:ok, site} =
Towerops.Sites.update_site(site, %{
snmp_community: "site-community",
snmp_version: "2c"
})
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_community: nil
})
device = Towerops.Repo.preload(device, site: :organization)
config = Devices.get_snmp_config(device)
assert config.community == "site-community"
assert config.source == :site
end
test "get_snmp_config/1 returns org-level community when device and site have nil", %{
organization: organization,
site: site
} do
# Update organization with SNMP community
{:ok, _organization} =
Towerops.Organizations.update_organization(organization, %{
snmp_community: "org-community",
snmp_version: "2c"
})
# Reload site with updated organization
site = Towerops.Repo.preload(site, :organization, force: true)
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_community: nil
})
device = Towerops.Repo.preload(device, site: :organization)
config = Devices.get_snmp_config(device)
assert config.community == "org-community"
assert config.source == :organization
end
test "get_snmp_config/1 returns nil when all levels have nil", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_community: nil
})
device = Towerops.Repo.preload(device, site: :organization)
config = Devices.get_snmp_config(device)
assert config.community == nil
assert config.source == :default
end
test "get_snmp_config/1 prioritizes device over site over org", %{
organization: organization,
site: site
} do
# Set community at all levels
{:ok, _organization} =
Towerops.Organizations.update_organization(organization, %{
snmp_community: "org-community",
snmp_version: "2c"
})
{:ok, site} =
Towerops.Sites.update_site(site, %{
snmp_community: "site-community",
snmp_version: "2c"
})
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_community: "device-community"
})
device = Towerops.Repo.preload(device, site: :organization)
config = Devices.get_snmp_config(device)
# Should use device-level community
assert config.community == "device-community"
assert config.source == :device
end
end
describe "events" do
import Towerops.AccountsFixtures
alias Towerops.Devices.Event
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} =
Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id
})
%{device: device}
end
test "create_event/1 creates an device event", %{device: device} do
attrs = %{
device_id: device.id,
event_type: "interface_up",
severity: "info",
message: "Interface came online",
metadata: %{"interface" => "eth0"},
occurred_at: DateTime.truncate(DateTime.utc_now(), :second)
}
assert {:ok, %Event{} = event} = Devices.create_event(attrs)
assert event.device_id == device.id
assert event.event_type == "interface_up"
assert event.severity == "info"
assert event.message == "Interface came online"
assert event.metadata == %{"interface" => "eth0"}
end
test "create_event/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Devices.create_event(%{})
end
test "list_devices_events/2 returns events for device ordered by most recent", %{
device: device
} do
now = DateTime.utc_now()
{:ok, event1} =
Devices.create_event(%{
device_id: device.id,
event_type: "interface_up",
severity: "info",
message: "Interface came up",
occurred_at: DateTime.add(now, -3600, :second)
})
{:ok, event2} =
Devices.create_event(%{
device_id: device.id,
event_type: "interface_down",
severity: "warning",
message: "Interface went down",
occurred_at: DateTime.add(now, -1800, :second)
})
events = Devices.list_devices_events(device.id)
assert length(events) == 2
# Should be ordered by most recent first
assert hd(events).id == event2.id
assert List.last(events).id == event1.id
end
test "list_devices_events/2 limits results", %{device: device} do
now = DateTime.utc_now()
# Create 10 events
for i <- 1..10 do
Devices.create_event(%{
device_id: device.id,
event_type: "interface_down",
severity: "warning",
message: "Interface down event #{i}",
occurred_at: DateTime.add(now, -i * 60, :second)
})
end
events = Devices.list_devices_events(device.id, 5)
assert length(events) == 5
end
end
describe "additional coverage tests" do
import Towerops.AccountsFixtures
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
})
%{organization: organization, site: site, user: user}
end
test "get_device/1 returns device when exists", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id
})
found = Devices.get_device(device.id)
assert found
assert found.id == device.id
end
test "get_device/1 returns nil when not exists" do
assert Devices.get_device(Ecto.UUID.generate()) == nil
end
test "get_device_with_details/1 preloads associations", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id
})
result = Devices.get_device_with_details(device.id)
assert result.id == device.id
assert Ecto.assoc_loaded?(result.site)
end
@tag :skip
test "delete_device/1 deletes the device", %{site: site} do
# Skipped: Requires Monitoring.Supervisor to be running
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id
})
assert {:ok, _deleted} = Devices.delete_device(device)
assert Devices.get_device(device.id) == nil
end
test "count_organization_devices/1 returns correct count", %{organization: organization, site: site} do
{:ok, _device1} =
Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id
})
{:ok, _device2} =
Devices.create_device(%{
name: "Router 2",
ip_address: "192.168.1.2",
site_id: site.id
})
assert Devices.count_organization_devices(organization.id) == 2
end
test "count_site_devices/1 returns correct count", %{site: site} do
{:ok, _device1} =
Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id
})
{:ok, _device2} =
Devices.create_device(%{
name: "Router 2",
ip_address: "192.168.1.2",
site_id: site.id
})
assert Devices.count_site_devices(site.id) == 2
end
test "count_site_devices_down/1 returns correct count", %{site: site} do
{:ok, device1} =
Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id
})
{:ok, _device2} =
Devices.create_device(%{
name: "Router 2",
ip_address: "192.168.1.2",
site_id: site.id
})
# Mark one as down
{:ok, _} = Devices.update_device_status(device1, :down)
assert Devices.count_site_devices_down(site.id) == 1
end
test "list_organization_devices/2 filters by site_id", %{organization: organization, site: site} do
{:ok, site2} =
Towerops.Sites.create_site(%{
name: "Site 2",
organization_id: organization.id
})
{:ok, device1} =
Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id
})
{:ok, _device2} =
Devices.create_device(%{
name: "Router 2",
ip_address: "192.168.1.2",
site_id: site2.id
})
result = Devices.list_organization_devices(organization.id, %{"site_id" => site.id})
assert length(result) == 1
assert hd(result).id == device1.id
end
test "list_organization_devices/2 filters by status", %{organization: organization, site: site} do
{:ok, device1} =
Devices.create_device(%{
name: "Router 1",
ip_address: "192.168.1.1",
site_id: site.id
})
{:ok, _device2} =
Devices.create_device(%{
name: "Router 2",
ip_address: "192.168.1.2",
site_id: site.id
})
# Mark one as up
Devices.update_device_status(device1, :up)
result = Devices.list_organization_devices(organization.id, %{"status" => "up"})
assert length(result) == 1
assert hd(result).id == device1.id
end
test "get_snmp_config/1 with device ID string", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_community: "test-community"
})
config = Devices.get_snmp_config(device.id)
assert config.community == "test-community"
assert config.source == :device
end
end
describe "automatic rediscovery on device updates" do
import Towerops.AccountsFixtures
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
})
%{organization: organization, site: site, user: user}
end
test "triggers discovery when snmp_enabled changes from false to true", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: false
})
# Clear any existing jobs
Towerops.Repo.delete_all(Oban.Job)
# Update to enable SNMP
{:ok, _updated} = Devices.update_device(device, %{snmp_enabled: true})
# Check that a discovery job was enqueued
jobs =
Oban.Job
|> Towerops.Repo.all()
|> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))
assert [job] = jobs
assert job.args["device_id"] == device.id
end
test "triggers discovery when snmp_version changes while enabled", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_version: "2c"
})
# Clear any existing jobs
Towerops.Repo.delete_all(Oban.Job)
# Update SNMP version
{:ok, _updated} = Devices.update_device(device, %{snmp_version: "1"})
# Check that a discovery job was enqueued
jobs =
Oban.Job
|> Towerops.Repo.all()
|> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))
assert [job] = jobs
assert job.args["device_id"] == device.id
end
test "triggers discovery when snmp_port changes while enabled", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_port: 161
})
# Clear any existing jobs
Towerops.Repo.delete_all(Oban.Job)
# Update SNMP port
{:ok, _updated} = Devices.update_device(device, %{snmp_port: 1161})
# Check that a discovery job was enqueued
jobs =
Oban.Job
|> Towerops.Repo.all()
|> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))
assert [job] = jobs
assert job.args["device_id"] == device.id
end
test "triggers discovery when monitoring_enabled changes to true with SNMP enabled", %{
site: site
} do
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
monitoring_enabled: false
})
# Clear any existing jobs
Towerops.Repo.delete_all(Oban.Job)
# Enable monitoring
{:ok, _updated} = Devices.update_device(device, %{monitoring_enabled: true})
# Check that a discovery job was enqueued
jobs =
Oban.Job
|> Towerops.Repo.all()
|> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))
assert [job] = jobs
assert job.args["device_id"] == device.id
end
test "does not trigger discovery when monitoring_enabled changes but SNMP disabled", %{
site: site
} do
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: false,
monitoring_enabled: false
})
# Clear any existing jobs
Towerops.Repo.delete_all(Oban.Job)
# Enable monitoring (but SNMP is disabled)
{:ok, _updated} = Devices.update_device(device, %{monitoring_enabled: true})
# Check that NO discovery job was enqueued
jobs =
Oban.Job
|> Towerops.Repo.all()
|> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))
assert [] = jobs
end
test "does not trigger discovery when SNMP is disabled", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_version: "2c"
})
# Clear any existing jobs
Towerops.Repo.delete_all(Oban.Job)
# Disable SNMP
{:ok, _updated} = Devices.update_device(device, %{snmp_enabled: false})
# Check that NO discovery job was enqueued (SNMP is disabled)
jobs =
Oban.Job
|> Towerops.Repo.all()
|> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))
assert [] = jobs
end
test "does not trigger discovery for unrelated field changes", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true
})
# Clear any existing jobs
Towerops.Repo.delete_all(Oban.Job)
# Update unrelated fields (name, description)
{:ok, _updated} = Devices.update_device(device, %{name: "Updated Router"})
# Check that NO discovery job was enqueued
jobs =
Oban.Job
|> Towerops.Repo.all()
|> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))
assert [] = jobs
end
test "does not trigger discovery when snmp_version changes while disabled", %{site: site} do
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: false,
snmp_version: "2c"
})
# Clear any existing jobs
Towerops.Repo.delete_all(Oban.Job)
# Update SNMP version (but SNMP is disabled)
{:ok, _updated} = Devices.update_device(device, %{snmp_version: "1"})
# Check that NO discovery job was enqueued (SNMP is disabled)
jobs =
Oban.Job
|> Towerops.Repo.all()
|> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))
assert [] = jobs
end
end
describe "property-based tests" do
import Towerops.AccountsFixtures
property "creating devices with valid IPs always succeeds" do
# Create fixtures once outside the property check
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
})
check all(
name <- string(:alphanumeric, min_length: 2, max_length: 20),
octet1 <- integer(0..255),
octet2 <- integer(0..255),
octet3 <- integer(0..255),
octet4 <- integer(1..254),
max_runs: 20
) do
ip_address = "#{octet1}.#{octet2}.#{octet3}.#{octet4}"
attrs = %{
name: name,
ip_address: ip_address,
site_id: site.id
}
# Use bypass_limits since property tests may create > 10 devices
assert {:ok, %Device{} = device} = Devices.create_device(attrs, bypass_limits: true)
assert device.ip_address == ip_address
assert device.status == :unknown
end
end
property "device status changes are idempotent" do
check all(status <- member_of([:up, :down, :unknown]), max_runs: 10) 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} =
Devices.create_device(%{
name: "Test Device",
ip_address: "192.168.1.100",
site_id: site.id
})
# Update status twice
{:ok, updated1} = Devices.update_device_status(device, status)
{:ok, updated2} = Devices.update_device_status(updated1, status)
assert updated2.status == status
# Second update should not change last_status_change_at
assert updated2.last_status_change_at == updated1.last_status_change_at
end
end
property "device counters are always non-negative" do
check all(device_count <- integer(0..5), max_runs: 10) 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
})
# Create specified number of devices
if device_count > 0 do
for i <- 1..device_count do
Devices.create_device(%{
name: "Device #{i}",
ip_address: "192.168.1.#{i}",
site_id: site.id
})
end
end
org_count = Devices.count_organization_devices(organization.id)
site_count = Devices.count_site_devices(site.id)
down_count = Devices.count_site_devices_down(site.id)
assert org_count >= 0
assert site_count >= 0
assert down_count >= 0
assert down_count <= site_count
assert org_count == site_count
end
end
property "SNMP config prioritization is consistent" do
check all(
device_community <- one_of([constant(nil), string(:alphanumeric, min_length: 1, max_length: 20)]),
site_community <- one_of([constant(nil), string(:alphanumeric, min_length: 1, max_length: 20)]),
max_runs: 10
) 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
})
# Update site with community if provided
site =
if site_community do
{:ok, updated_site} = Towerops.Sites.update_site(site, %{snmp_community: site_community})
updated_site
else
site
end
{:ok, device} =
Devices.create_device(%{
name: "Router",
ip_address: "192.168.1.1",
site_id: site.id,
snmp_enabled: true,
snmp_community: device_community
})
device = Towerops.Repo.preload(device, site: :organization)
config = Devices.get_snmp_config(device)
# Device-level always takes precedence if set
if device_community do
assert config.community == device_community
assert config.source == :device
else
if site_community do
assert config.community == site_community
assert config.source == :site
else
assert config.source in [:organization, :default]
end
end
end
end
end
describe "device creation with subscription limits" do
import Towerops.AccountsFixtures
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
})
%{organization: organization, site: site, user: user}
end
test "blocks device creation when at 10-device limit", %{site: site} do
# Create 10 devices (the limit)
for i <- 1..10 do
{:ok, _device} =
Devices.create_device(%{
name: "Device #{i}",
ip_address: "192.168.1.#{i}",
site_id: site.id,
monitoring_enabled: false,
snmp_enabled: false
})
end
# 11th device should be blocked
assert {:error, changeset} =
Devices.create_device(%{
name: "Device 11",
ip_address: "192.168.1.11",
site_id: site.id,
monitoring_enabled: false,
snmp_enabled: false
})
assert %{base: ["You've reached your plan limit of 10 devices. Upgrade to add more."]} =
errors_on(changeset)
end
test "allows device creation when under limit", %{site: site} do
# Create 9 devices
for i <- 1..9 do
{:ok, _device} =
Devices.create_device(%{
name: "Device #{i}",
ip_address: "192.168.1.#{i}",
site_id: site.id,
monitoring_enabled: false,
snmp_enabled: false
})
end
# 10th device should succeed
assert {:ok, _device} =
Devices.create_device(%{
name: "Device 10",
ip_address: "192.168.1.10",
site_id: site.id,
monitoring_enabled: false,
snmp_enabled: false
})
end
test "bypasses limit with bypass_limits option", %{site: site} do
# Create 10 devices (at limit)
for i <- 1..10 do
{:ok, _device} =
Devices.create_device(%{
name: "Device #{i}",
ip_address: "192.168.1.#{i}",
site_id: site.id,
monitoring_enabled: false,
snmp_enabled: false
})
end
# 11th device succeeds with bypass
assert {:ok, _device} =
Devices.create_device(
%{
name: "Device 11",
ip_address: "192.168.1.11",
site_id: site.id,
monitoring_enabled: false,
snmp_enabled: false
},
bypass_limits: true
)
end
test "counts devices across multiple sites in same organization", %{
organization: organization,
site: site1
} do
{:ok, site2} =
Towerops.Sites.create_site(%{
name: "Site 2",
organization_id: organization.id
})
# Create 6 devices in site1
for i <- 1..6 do
{:ok, _device} =
Devices.create_device(%{
name: "Site1 Device #{i}",
ip_address: "192.168.1.#{i}",
site_id: site1.id,
monitoring_enabled: false,
snmp_enabled: false
})
end
# Create 4 devices in site2 (total = 10)
for i <- 1..4 do
{:ok, _device} =
Devices.create_device(%{
name: "Site2 Device #{i}",
ip_address: "192.168.2.#{i}",
site_id: site2.id,
monitoring_enabled: false,
snmp_enabled: false
})
end
# 11th device in either site should be blocked
assert {:error, changeset} =
Devices.create_device(%{
name: "Site1 Device 7",
ip_address: "192.168.1.7",
site_id: site1.id,
monitoring_enabled: false,
snmp_enabled: false
})
assert %{base: ["You've reached your plan limit of 10 devices. Upgrade to add more."]} =
errors_on(changeset)
end
end
end