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