towerops/test/mix/tasks/import_profiles_test.exs
2026-06-15 11:15:46 -05:00

464 lines
15 KiB
Elixir

defmodule Mix.Tasks.ImportProfilesTest do
use Towerops.DataCase, async: true
import ExUnit.CaptureIO
alias Mix.Tasks.ImportProfiles
alias Towerops.Profiles
alias Towerops.Profiles.DeviceOid
alias Towerops.Profiles.Profile
alias Towerops.Profiles.SensorOid
alias Towerops.Repo
setup do
# Create temporary directory for test YAML files
temp_dir = Path.join(System.tmp_dir!(), "profiles_test_#{System.unique_integer([:positive])}")
detection_dir = Path.join([temp_dir, "os_detection"])
discovery_dir = Path.join([temp_dir, "os_discovery"])
File.mkdir_p!(detection_dir)
File.mkdir_p!(discovery_dir)
on_exit(fn ->
File.rm_rf!(temp_dir)
end)
{:ok, temp_dir: temp_dir, detection_dir: detection_dir, discovery_dir: discovery_dir}
end
describe "run/1 with valid profiles" do
test "imports a basic profile from YAML", %{
detection_dir: detection_dir,
discovery_dir: discovery_dir,
temp_dir: temp_dir
} do
# Create a simple detection file
detection_yaml = Path.join(detection_dir, "mikrotik.yaml")
File.write!(detection_yaml, """
os: mikrotik
text: MikroTik
priority: 100
discovery:
- sysDescr:
- RouterOS
- sysObjectID:
- .1.3.6.1.4.1.14988.
""")
# Create a simple discovery file
discovery_yaml = Path.join(discovery_dir, "mikrotik.yaml")
File.write!(discovery_yaml, """
modules:
os:
serial: SNMPv2-MIB::sysContact.0
version: SNMPv2-MIB::sysName.0
sensors:
temperature:
data:
- oid: MIKROTIK-MIB::mtxrHlTemperature.0
descr: System Temperature
unit: C
divisor: 10
""")
output =
capture_io(fn ->
ImportProfiles.run(["--source-path", temp_dir])
end)
assert output =~ "✓ mikrotik: created/updated successfully"
assert output =~ "Import complete: 1 succeeded, 0 failed"
# Verify profile was created
profile = Profiles.get_profile("mikrotik")
assert %Profile{} = profile
assert profile.vendor == "MikroTik"
assert profile.priority == 100
assert profile.enabled == true
# Verify device OIDs were created
device_oids = Repo.all(from o in DeviceOid, where: o.profile_id == ^profile.id)
assert length(device_oids) == 2
serial_oid = Enum.find(device_oids, &(&1.field == "serial_number"))
assert serial_oid.mib_name == "SNMPv2-MIB::sysContact.0"
version_oid = Enum.find(device_oids, &(&1.field == "firmware_version"))
assert version_oid.mib_name == "SNMPv2-MIB::sysName.0"
# Verify sensor OIDs were created
sensor_oids = Repo.all(from o in SensorOid, where: o.profile_id == ^profile.id)
assert length(sensor_oids) == 1
temp_sensor = List.first(sensor_oids)
assert temp_sensor.sensor_type == "temperature"
assert temp_sensor.mib_name == "MIKROTIK-MIB::mtxrHlTemperature.0"
assert temp_sensor.sensor_descr == "System Temperature"
assert temp_sensor.sensor_unit == "C"
assert temp_sensor.sensor_divisor == 10
end
test "updates existing profile", %{detection_dir: detection_dir, temp_dir: temp_dir} do
# Create initial profile
{:ok, profile} =
Profiles.create_profile(%{
name: "cisco",
vendor: "Cisco Old",
detection_pattern: "old pattern",
priority: 50,
enabled: true
})
# Create YAML file with updated info
detection_yaml = Path.join(detection_dir, "cisco.yaml")
File.write!(detection_yaml, """
os: cisco
text: Cisco Updated
priority: 200
discovery:
- sysDescr:
- Cisco IOS
""")
output =
capture_io(fn ->
ImportProfiles.run(["--source-path", temp_dir])
end)
assert output =~ "✓ cisco: created/updated successfully"
# Verify profile was updated
updated_profile = Profiles.get_profile("cisco")
assert updated_profile.id == profile.id
assert updated_profile.vendor == "Cisco Updated"
assert updated_profile.priority == 200
end
test "imports specific profiles only", %{detection_dir: detection_dir, temp_dir: temp_dir} do
# Create multiple detection files
File.write!(Path.join(detection_dir, "mikrotik.yaml"), """
os: mikrotik
text: MikroTik
priority: 100
""")
File.write!(Path.join(detection_dir, "cisco.yaml"), """
os: cisco
text: Cisco
priority: 100
""")
File.write!(Path.join(detection_dir, "ubiquiti.yaml"), """
os: ubiquiti
text: Ubiquiti
priority: 100
""")
output =
capture_io(fn ->
ImportProfiles.run(["--source-path", temp_dir, "--profiles", "mikrotik,cisco"])
end)
assert output =~ "Importing 2 specified profiles"
assert output =~ "✓ mikrotik"
assert output =~ "✓ cisco"
refute output =~ "ubiquiti"
# Verify only specified profiles were created
assert Profiles.get_profile("mikrotik")
assert Profiles.get_profile("cisco")
assert Profiles.get_profile("ubiquiti") == nil
end
test "handles profile without discovery file", %{detection_dir: detection_dir, temp_dir: temp_dir} do
detection_yaml = Path.join(detection_dir, "generic.yaml")
File.write!(detection_yaml, """
os: generic
text: Generic Device
priority: 1
""")
output =
capture_io(fn ->
ImportProfiles.run(["--source-path", temp_dir])
end)
assert output =~ "✓ generic: created/updated successfully"
profile = Profiles.get_profile("generic")
assert %Profile{} = profile
assert profile.vendor == "Generic Device"
# Should have no device OIDs or sensor OIDs
assert Repo.aggregate(from(o in DeviceOid, where: o.profile_id == ^profile.id), :count) == 0
assert Repo.aggregate(from(o in SensorOid, where: o.profile_id == ^profile.id), :count) == 0
end
test "extracts detection patterns from sysDescr", %{detection_dir: detection_dir, temp_dir: temp_dir} do
detection_yaml = Path.join(detection_dir, "test.yaml")
File.write!(detection_yaml, """
os: test
text: Test Device
discovery:
- sysDescr:
- "Test Pattern 1"
- "Test Pattern 2"
""")
capture_io(fn ->
ImportProfiles.run(["--source-path", temp_dir])
end)
profile = Profiles.get_profile("test")
assert profile.detection_pattern == "Test Pattern 1"
end
test "extracts detection OID from sysObjectID", %{detection_dir: detection_dir, temp_dir: temp_dir} do
detection_yaml = Path.join(detection_dir, "test.yaml")
File.write!(detection_yaml, """
os: test
text: Test Device
discovery:
- sysObjectID:
- ".1.3.6.1.4.1.9."
- ".1.3.6.1.4.1.10."
""")
capture_io(fn ->
ImportProfiles.run(["--source-path", temp_dir])
end)
profile = Profiles.get_profile("test")
assert profile.detection_oid == ".1.3.6.1.4.1.9."
end
test "imports all standard device OID fields", %{
detection_dir: detection_dir,
discovery_dir: discovery_dir,
temp_dir: temp_dir
} do
File.write!(Path.join(detection_dir, "test.yaml"), """
os: test
text: Test Device
""")
File.write!(Path.join(discovery_dir, "test.yaml"), """
modules:
os:
serial: DEVICE-MIB::serialNumber.0
version: DEVICE-MIB::firmwareVersion.0
hardware: DEVICE-MIB::hardwareModel.0
lat: DEVICE-MIB::latitude.0
long: DEVICE-MIB::longitude.0
features: DEVICE-MIB::features.0
sysName: SNMPv2-MIB::sysName.0
""")
capture_io(fn ->
ImportProfiles.run(["--source-path", temp_dir])
end)
profile = Profiles.get_profile("test")
device_oids = Repo.all(from o in DeviceOid, where: o.profile_id == ^profile.id)
assert length(device_oids) == 7
assert Enum.find(device_oids, &(&1.field == "serial_number")).mib_name == "DEVICE-MIB::serialNumber.0"
assert Enum.find(device_oids, &(&1.field == "firmware_version")).mib_name == "DEVICE-MIB::firmwareVersion.0"
assert Enum.find(device_oids, &(&1.field == "hardware")).mib_name == "DEVICE-MIB::hardwareModel.0"
assert Enum.find(device_oids, &(&1.field == "latitude")).mib_name == "DEVICE-MIB::latitude.0"
assert Enum.find(device_oids, &(&1.field == "longitude")).mib_name == "DEVICE-MIB::longitude.0"
assert Enum.find(device_oids, &(&1.field == "features")).mib_name == "DEVICE-MIB::features.0"
assert Enum.find(device_oids, &(&1.field == "sys_name_oid")).mib_name == "SNMPv2-MIB::sysName.0"
end
test "imports all sensor types", %{detection_dir: detection_dir, discovery_dir: discovery_dir, temp_dir: temp_dir} do
File.write!(Path.join(detection_dir, "test.yaml"), """
os: test
text: Test Device
""")
File.write!(Path.join(discovery_dir, "test.yaml"), """
modules:
sensors:
temperature:
data:
- oid: DEVICE-MIB::temp.0
descr: Temperature
unit: C
voltage:
data:
- oid: DEVICE-MIB::voltage.0
descr: Voltage
unit: V
current:
data:
- oid: DEVICE-MIB::current.0
descr: Current
unit: A
power:
data:
- oid: DEVICE-MIB::power.0
descr: Power
unit: W
fanspeed:
data:
- oid: DEVICE-MIB::fan.0
descr: Fan Speed
unit: RPM
""")
capture_io(fn ->
ImportProfiles.run(["--source-path", temp_dir])
end)
profile = Profiles.get_profile("test")
sensor_oids = Repo.all(from o in SensorOid, where: o.profile_id == ^profile.id)
assert length(sensor_oids) == 5
assert Enum.any?(sensor_oids, &(&1.sensor_type == "temperature"))
assert Enum.any?(sensor_oids, &(&1.sensor_type == "voltage"))
assert Enum.any?(sensor_oids, &(&1.sensor_type == "current"))
assert Enum.any?(sensor_oids, &(&1.sensor_type == "power"))
assert Enum.any?(sensor_oids, &(&1.sensor_type == "fanspeed"))
end
test "skips table sensors with {{ $index }}", %{
detection_dir: detection_dir,
discovery_dir: discovery_dir,
temp_dir: temp_dir
} do
File.write!(Path.join(detection_dir, "test.yaml"), """
os: test
text: Test Device
""")
File.write!(Path.join(discovery_dir, "test.yaml"), """
modules:
sensors:
temperature:
data:
- oid: DEVICE-MIB::temp.0
descr: Scalar Temperature
unit: C
- oid: DEVICE-MIB::tempTable
num_oid: .1.3.6.1.4.1.9999.1.2.{{ $index }}
descr: Table Temperature
unit: C
""")
capture_io(fn ->
ImportProfiles.run(["--source-path", temp_dir])
end)
profile = Profiles.get_profile("test")
sensor_oids = Repo.all(from o in SensorOid, where: o.profile_id == ^profile.id)
# Only scalar sensor should be imported
assert length(sensor_oids) == 1
assert List.first(sensor_oids).sensor_descr == "Scalar Temperature"
end
test "replaces existing OIDs when reimporting", %{
detection_dir: detection_dir,
discovery_dir: discovery_dir,
temp_dir: temp_dir
} do
# Create initial profile with OIDs
{:ok, profile} = Profiles.create_profile(%{name: "test", vendor: "Test", priority: 100, enabled: true})
{:ok, _} = Profiles.add_device_oid(profile.id, "serial_number", "OLD-MIB::serial.0")
{:ok, _} =
Profiles.add_sensor_oid(profile.id, %{
sensor_type: "temperature",
mib_name: "OLD-MIB::temp.0",
sensor_descr: "Old Temp",
sensor_unit: "C",
sensor_divisor: 1
})
# Create new YAML with different OIDs
File.write!(Path.join(detection_dir, "test.yaml"), """
os: test
text: Test Device
""")
File.write!(Path.join(discovery_dir, "test.yaml"), """
modules:
os:
serial: NEW-MIB::serialNumber.0
sensors:
voltage:
data:
- oid: NEW-MIB::voltage.0
descr: New Voltage
unit: V
""")
capture_io(fn ->
ImportProfiles.run(["--source-path", temp_dir])
end)
# Old OIDs should be replaced
device_oids = Repo.all(from o in DeviceOid, where: o.profile_id == ^profile.id)
assert length(device_oids) == 1
assert List.first(device_oids).mib_name == "NEW-MIB::serialNumber.0"
sensor_oids = Repo.all(from o in SensorOid, where: o.profile_id == ^profile.id)
assert length(sensor_oids) == 1
assert List.first(sensor_oids).mib_name == "NEW-MIB::voltage.0"
assert List.first(sensor_oids).sensor_type == "voltage"
end
end
describe "run/1 error handling" do
test "raises error when source directory doesn't exist" do
assert_raise RuntimeError, "Source directory not found: /nonexistent/path", fn ->
capture_io(fn ->
ImportProfiles.run(["--source-path", "/nonexistent/path"])
end)
end
end
test "reports failed imports", %{detection_dir: detection_dir, temp_dir: temp_dir} do
# Create invalid YAML
File.write!(Path.join(detection_dir, "invalid.yaml"), """
this is not: [valid yaml: that will parse
""")
output =
capture_io([capture_prompt: false], fn ->
ImportProfiles.run(["--source-path", temp_dir])
end)
assert output =~ ~r/✗ invalid|Import complete: 0 succeeded, 1 failed/
assert output =~ "Import complete: 0 succeeded, 1 failed"
end
test "uses default source path when not provided" do
# Use explicit --source-path with empty temp dir to avoid scanning 788+ real profile files
# Testing the no-args behavior would scan all real files which is slow
temp_dir = Path.join(System.tmp_dir!(), "import_profiles_test_#{:rand.uniform(999_999)}")
File.mkdir_p!(Path.join(temp_dir, "os_detection"))
try do
output =
capture_io(fn ->
# Use --source-path to avoid slow scan of real priv/profiles directory
ImportProfiles.run(["--source-path", temp_dir])
end)
# Should discover files and find none
assert output =~ ~r/Discovering YAML files|Found 0 YAML files/
after
File.rm_rf!(temp_dir)
end
end
end
end