Files
netbox-librenms-plugin/netbox_librenms_plugin/tests/test_coverage_device_operations.py
Vlastislav Svatek 673e67106e
Some checks failed
ci / deploy (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
first commit
2026-06-05 10:39:05 +02:00

1668 lines
70 KiB
Python

"""Coverage tests for import_utils/device_operations.py."""
from unittest.mock import MagicMock, patch
class TestTryChassisDeviceTypeMatch:
"""Tests for _try_chassis_device_type_match (lines 45-65)."""
def test_api_failure_returns_none(self):
from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match
api = MagicMock()
api.get_inventory_filtered.return_value = (False, [])
result = _try_chassis_device_type_match(api, 1)
assert result is None
def test_empty_inventory_returns_none(self):
from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match
api = MagicMock()
api.get_inventory_filtered.return_value = (True, [])
result = _try_chassis_device_type_match(api, 1)
assert result is None
def test_matched_physical_name_returns_match(self):
from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match
mock_dt = MagicMock()
api = MagicMock()
api.get_inventory_filtered.return_value = (
True,
[{"entPhysicalName": "CHAS-BP-MX480-S", "entPhysicalModelName": "model1"}],
)
with patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type"
) as mock_match:
mock_match.return_value = {"matched": True, "device_type": mock_dt, "match_type": "exact"}
result = _try_chassis_device_type_match(api, 1)
assert result is not None
assert result["matched"] is True
assert result["match_type"] == "chassis"
assert result["chassis_model"] == "CHAS-BP-MX480-S"
def test_skips_empty_values(self):
from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match
api = MagicMock()
api.get_inventory_filtered.return_value = (True, [{"entPhysicalName": "", "entPhysicalModelName": "-"}])
with patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type"
) as mock_match:
mock_match.return_value = {"matched": False}
result = _try_chassis_device_type_match(api, 1)
mock_match.assert_not_called()
assert result is None
def test_exception_returns_none(self):
from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match
api = MagicMock()
api.get_inventory_filtered.side_effect = RuntimeError("API Error")
result = _try_chassis_device_type_match(api, 1)
assert result is None
def test_fallback_to_model_name_when_name_not_matched(self):
from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match
mock_dt = MagicMock()
api = MagicMock()
api.get_inventory_filtered.return_value = (
True,
[{"entPhysicalName": "Unrecognized", "entPhysicalModelName": "710-017414"}],
)
call_count = [0]
def match_side_effect(value):
call_count[0] += 1
if value == "Unrecognized":
return {"matched": False}
return {"matched": True, "device_type": mock_dt, "match_type": "exact"}
with patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
side_effect=match_side_effect,
):
result = _try_chassis_device_type_match(api, 1)
assert result is not None
assert result["matched"] is True
assert result["chassis_model"] == "710-017414"
def test_match_returning_none_is_skipped(self):
"""match_librenms_hardware_to_device_type returning None does not raise; continues."""
from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match
api = MagicMock()
api.get_inventory_filtered.return_value = (
True,
[{"entPhysicalName": "SomeChassis", "entPhysicalModelName": ""}],
)
with patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
return_value=None,
):
result = _try_chassis_device_type_match(api, 1)
# None return from matcher is safely skipped; function returns None overall
assert result is None
class TestDetermineDeviceName:
"""Tests for _determine_device_name (lines 68-122)."""
def test_use_sysname_true_prefers_sysname(self):
from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name
result = _determine_device_name({"sysName": "router01", "hostname": "router01.example.com"}, use_sysname=True)
assert result == "router01"
def test_use_sysname_false_prefers_hostname(self):
from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name
result = _determine_device_name({"sysName": "router01", "hostname": "router01.example.com"}, use_sysname=False)
assert result == "router01.example.com"
def test_fallback_to_device_id_when_no_name(self):
from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name
result = _determine_device_name({}, device_id=42)
assert result == "device-42"
def test_fallback_to_device_id_field_when_no_name_no_id(self):
from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name
result = _determine_device_name({"device_id": 99})
assert result == "device-99"
def test_strip_domain_true_strips_suffix(self):
from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name
result = _determine_device_name({"sysName": "router01.example.com"}, strip_domain=True)
assert result == "router01"
def test_strip_domain_does_not_strip_ip(self):
from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name
result = _determine_device_name({"sysName": "192.168.1.1"}, strip_domain=True)
assert result == "192.168.1.1"
def test_hostname_fallback_when_sysname_empty(self):
from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name
result = _determine_device_name({"sysName": "", "hostname": "sw01.example.com"}, use_sysname=True)
assert result == "sw01.example.com"
def test_none_sysname_falls_back_to_hostname(self):
from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name
result = _determine_device_name({"sysName": None, "hostname": "sw02"})
assert result == "sw02"
def test_none_hostname_falls_back_to_device_id(self):
from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name
result = _determine_device_name({"sysName": None, "hostname": None, "device_id": 7})
assert result == "device-7"
def test_result_is_never_empty_string(self):
from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name
result = _determine_device_name({})
assert isinstance(result, str)
assert result != ""
def test_fqdn_multiple_dots_strips_to_first_label(self):
from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name
result = _determine_device_name({"sysName": "a.b.c.d.example.com"}, strip_domain=True)
assert result == "a"
def test_strip_domain_false_keeps_fqdn(self):
from netbox_librenms_plugin.import_utils.device_operations import _determine_device_name
result = _determine_device_name({"sysName": "router.example.com"}, strip_domain=False)
assert result == "router.example.com"
class TestValidateDeviceStateMachine:
"""Tests for validate_device_for_import is_ready / can_import state transitions."""
def _run_validate(self, libre_device, patches_overrides=None, **kwargs):
from unittest.mock import MagicMock, patch
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
api = MagicMock()
api.server_key = "default"
api.cache_timeout = 300
mock_device = MagicMock()
mock_device.objects.filter.return_value.first.return_value = None
mock_device.objects.filter.return_value.exclude.return_value.first.return_value = None
mock_vm = MagicMock()
mock_vm.objects.filter.return_value.first.return_value = None
mock_cluster = MagicMock()
mock_cluster.objects.all.return_value = []
mock_role = MagicMock()
mock_role.objects.all.return_value = []
mock_ip = MagicMock()
mock_ip.objects.filter.return_value.first.return_value = None
base_patches = [
patch(
"netbox_librenms_plugin.import_utils.device_operations.find_matching_site",
return_value={"found": False, "site": None, "match_type": None, "suggestions": []},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
return_value={"matched": False, "device_type": None, "match_type": None},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.find_matching_platform",
return_value={"found": False, "platform": None, "match_type": None},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data",
return_value={"is_stack": False, "member_count": 0, "members": []},
),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole", mock_role),
patch("netbox_librenms_plugin.import_utils.device_operations.Cluster", mock_cluster),
patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType", MagicMock()),
patch("netbox_librenms_plugin.import_utils.device_operations.Site", MagicMock()),
patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id", return_value=None),
patch("netbox_librenms_plugin.import_utils.device_operations.cache"),
patch("virtualization.models.VirtualMachine", mock_vm),
]
if patches_overrides:
base_patches.extend(patches_overrides)
for p in base_patches:
p.start()
try:
result = validate_device_for_import(libre_device, api=api, **kwargs)
finally:
for p in reversed(base_patches):
p.stop()
return result
def _base_device(self, device_id=1, hostname="router01"):
return {
"device_id": device_id,
"hostname": hostname,
"sysName": hostname,
"hardware": "-",
"serial": "-",
"os": "-",
"location": "-",
}
def test_new_device_without_any_matches_is_not_ready(self):
"""New device with no site/type/role match must not be ready."""
result = self._run_validate(self._base_device())
assert result["existing_device"] is None
assert result["is_ready"] is False
assert result["can_import"] is False
def test_new_vm_without_cluster_is_not_ready(self):
"""New VM import with no cluster available must not be ready."""
result = self._run_validate(self._base_device(hostname="vm01"), import_as_vm=True)
assert result["is_ready"] is False
assert result.get("import_as_vm") is True
assert result["cluster"]["found"] is False
def test_new_device_site_and_type_found_but_role_manual(self):
"""
New device with site+type matched still requires manual role selection.
validate_device_for_import always sets device_role["found"]=False for new
devices and adds an issue. is_ready becomes True only AFTER the user selects
a role via apply_role_to_validation + recalculate_validation_status.
"""
from unittest.mock import MagicMock, patch
site_mock = MagicMock()
dt_mock = MagicMock()
role_mock = MagicMock()
role_mock.pk = 1
result = self._run_validate(
self._base_device(),
patches_overrides=[
patch(
"netbox_librenms_plugin.import_utils.device_operations.find_matching_site",
return_value={"found": True, "site": site_mock, "match_type": "exact", "suggestions": []},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
return_value={"matched": True, "device_type": dt_mock, "match_type": "exact"},
),
],
)
# Site and type found, but role is always manual for new devices
assert result["site"]["found"] is True
assert result["device_type"]["found"] is True
assert result["device_role"]["found"] is False
assert result["can_import"] is False # blocked by role issue
# Simulate user selecting a role via apply_role_to_validation
from netbox_librenms_plugin.import_validation_helpers import (
apply_role_to_validation,
recalculate_validation_status,
)
apply_role_to_validation(result, role=role_mock, is_vm=False)
recalculate_validation_status(result, is_vm=False)
# After role selection, device should be ready
assert result["device_role"]["found"] is True
assert result["can_import"] is True
assert result["is_ready"] is True
def test_is_ready_false_when_site_missing_even_with_type_and_role(self):
"""is_ready requires ALL of site+type+role; missing site -> False."""
from unittest.mock import MagicMock, patch
dt_mock = MagicMock()
role_mock = MagicMock()
result = self._run_validate(
self._base_device(),
patches_overrides=[
patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
return_value={"matched": True, "device_type": dt_mock, "match_type": "exact"},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.DeviceRole",
MagicMock(objects=MagicMock(all=MagicMock(return_value=[role_mock]))),
),
],
)
assert result["is_ready"] is False
assert result["site"]["found"] is False
def test_import_as_vm_skips_device_only_fields(self):
"""VM import path must not fail on device-only type fields."""
from unittest.mock import MagicMock, patch
dt_mock = MagicMock()
result = self._run_validate(
self._base_device(hostname="vm01"),
import_as_vm=True,
patches_overrides=[
patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
return_value={"matched": True, "device_type": dt_mock, "match_type": "exact"},
),
],
)
assert result.get("import_as_vm") is True
assert result["is_ready"] is False # no cluster found
class TestGetLibreNMSDeviceById:
"""Tests for get_librenms_device_by_id (lines 912-933)."""
def test_success_returns_device(self):
from netbox_librenms_plugin.import_utils.device_operations import get_librenms_device_by_id
api = MagicMock()
device = {"device_id": 42, "hostname": "router01"}
api.get_device_info.return_value = (True, device)
result = get_librenms_device_by_id(api, 42)
assert result is device
def test_api_failure_returns_none(self):
from netbox_librenms_plugin.import_utils.device_operations import get_librenms_device_by_id
api = MagicMock()
api.get_device_info.return_value = (False, None)
result = get_librenms_device_by_id(api, 42)
assert result is None
def test_device_not_found_returns_none(self):
from netbox_librenms_plugin.import_utils.device_operations import get_librenms_device_by_id
api = MagicMock()
api.get_device_info.return_value = (True, None)
result = get_librenms_device_by_id(api, 42)
assert result is None
def test_exception_returns_none(self):
from netbox_librenms_plugin.import_utils.device_operations import get_librenms_device_by_id
api = MagicMock()
api.get_device_info.side_effect = RuntimeError("Network error")
result = get_librenms_device_by_id(api, 42)
assert result is None
class TestFetchDeviceWithCache:
"""Tests for fetch_device_with_cache (lines 936-987)."""
@patch("netbox_librenms_plugin.import_utils.device_operations.cache")
def test_from_pre_fetched_cache_dict(self, mock_cache):
from netbox_librenms_plugin.import_utils.device_operations import fetch_device_with_cache
api = MagicMock()
api.server_key = "default"
device = {"device_id": 1}
cache_dict = {1: device}
result = fetch_device_with_cache(1, api, libre_devices_cache=cache_dict)
assert result is device
mock_cache.get.assert_not_called()
@patch("netbox_librenms_plugin.import_utils.device_operations.cache")
def test_from_django_cache(self, mock_cache):
from netbox_librenms_plugin.import_utils.device_operations import fetch_device_with_cache
api = MagicMock()
api.server_key = "default"
device = {"device_id": 1}
mock_cache.get.return_value = device
result = fetch_device_with_cache(1, api)
assert result is device
api.get_device_info.assert_not_called()
@patch("netbox_librenms_plugin.import_utils.device_operations.cache")
def test_cache_miss_falls_back_to_api(self, mock_cache):
from netbox_librenms_plugin.import_utils.device_operations import fetch_device_with_cache
api = MagicMock()
api.server_key = "default"
api.cache_timeout = 300
device = {"device_id": 1}
mock_cache.get.return_value = None
api.get_device_info.return_value = (True, device)
result = fetch_device_with_cache(1, api)
assert result is device
mock_cache.set.assert_called_once()
@patch("netbox_librenms_plugin.import_utils.device_operations.cache")
def test_api_returns_none_returns_none(self, mock_cache):
from netbox_librenms_plugin.import_utils.device_operations import fetch_device_with_cache
api = MagicMock()
api.server_key = "default"
mock_cache.get.return_value = None
api.get_device_info.return_value = (False, None)
result = fetch_device_with_cache(1, api)
assert result is None
@patch("netbox_librenms_plugin.import_utils.device_operations.cache")
def test_uses_provided_server_key(self, mock_cache):
from netbox_librenms_plugin.import_utils.device_operations import fetch_device_with_cache
api = MagicMock()
api.server_key = "default"
api.cache_timeout = 300
mock_cache.get.return_value = None
api.get_device_info.return_value = (True, {"device_id": 1})
fetch_device_with_cache(1, api, server_key="secondary")
# The cache key should use "secondary"
cache_key = mock_cache.get.call_args[0][0]
assert "secondary" in cache_key
class TestValidateDeviceForImport:
"""Tests for validate_device_for_import main validation logic."""
def _make_api(self):
api = MagicMock()
api.server_key = "default"
api.cache_timeout = 300
api.get_device_info.return_value = (True, {"device_id": 1})
return api
def _patch_all_db(self):
"""Context manager patches for all DB interactions."""
mock_device = MagicMock()
mock_device.objects.filter.return_value.first.return_value = None
mock_device.objects.filter.return_value.exclude.return_value.first.return_value = None
mock_device.objects.filter.return_value.select_for_update.return_value.filter.return_value.exclude.return_value.first.return_value = None
mock_device.objects.all.return_value = []
mock_vm = MagicMock()
mock_vm.objects.filter.return_value.first.return_value = None
mock_cluster = MagicMock()
mock_cluster.objects.all.return_value = []
mock_device_role = MagicMock()
mock_device_role.objects.all.return_value = []
mock_site = MagicMock()
mock_site.objects.all.return_value = []
mock_ip = MagicMock()
mock_ip.objects.filter.return_value.first.return_value = None
patches = [
patch(
"netbox_librenms_plugin.import_utils.device_operations.find_matching_site",
return_value={"found": False, "site": None, "match_type": None, "suggestions": []},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
return_value={"matched": False, "device_type": None, "match_type": None},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.find_matching_platform",
return_value={"found": False, "platform": None, "match_type": None},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data",
return_value={"is_stack": False, "member_count": 0, "members": []},
),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole", mock_device_role),
patch("netbox_librenms_plugin.import_utils.device_operations.Cluster", mock_cluster),
patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType", MagicMock()),
patch("netbox_librenms_plugin.import_utils.device_operations.Site", mock_site),
patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id", return_value=None),
patch("netbox_librenms_plugin.import_utils.device_operations.cache"),
patch("virtualization.models.VirtualMachine", mock_vm),
patch("ipam.models.IPAddress", mock_ip),
]
return patches
def test_minimal_device_validation(self):
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
libre_device = {
"device_id": 1,
"hostname": "router01",
"sysName": "router01",
"hardware": "-",
"serial": "-",
"os": "-",
"location": "-",
"type": "network",
}
api = self._make_api()
patches = self._patch_all_db()
try:
for p in patches:
p.start()
result = validate_device_for_import(libre_device, api=api)
finally:
for p in patches:
p.stop()
assert result is not None
assert "status" in result or "is_ready" in result
def test_vm_import_uses_correct_model(self):
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
libre_device = {
"device_id": 1,
"hostname": "vm01",
"sysName": "vm01",
"hardware": "-",
"serial": "-",
"os": "-",
"location": "-",
"type": "network",
}
api = self._make_api()
patches = self._patch_all_db()
try:
for p in patches:
p.start()
result = validate_device_for_import(libre_device, import_as_vm=True, api=api)
finally:
for p in patches:
p.stop()
assert result is not None
assert result.get("import_as_vm") is True
def test_existing_device_detected(self):
"""When device with same librenms_id exists, sets existing_device in result."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
libre_device = {
"device_id": 1,
"hostname": "router01",
"sysName": "router01",
"hardware": "-",
"serial": "-",
"os": "-",
"location": "-",
}
api = self._make_api()
existing = MagicMock()
existing.name = "router01"
existing.serial = ""
patches = self._patch_all_db()
# Override find_by_librenms_id: return None for VM, existing for Device
try:
for p in patches:
p.start()
def _find_side_effect(model, device_id, server_key):
from virtualization.models import VirtualMachine as VM
return None if model is VM else existing
with patch(
"netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id",
side_effect=_find_side_effect,
):
result = validate_device_for_import(libre_device, api=api)
finally:
for p in patches:
p.stop()
assert result.get("existing_device") is existing
class TestImportSingleDevice:
"""Tests for import_single_device (lines 689-910)."""
def _make_libre_device(self):
return {
"device_id": 1,
"hostname": "router01",
"sysName": "router01",
"hardware": "Cisco",
"serial": "SN001",
"os": "ios",
"status": 1,
"location": "-",
}
@patch("netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI")
def test_missing_site_returns_error(self, MockAPI):
from netbox_librenms_plugin.import_utils.device_operations import import_single_device
libre_device = self._make_libre_device()
validation = {
"existing_device": None,
"site": {"found": False, "site": None},
"device_type": {"matched": True, "device_type": MagicMock()},
"device_role": {"found": True, "role": MagicMock()},
"platform": {"found": False, "platform": None},
"rack": {"rack": None},
}
with (
patch("netbox_librenms_plugin.import_utils.device_operations.Site"),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType"),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole"),
patch("netbox_librenms_plugin.import_utils.device_operations.Rack"),
):
result = import_single_device(1, server_key="default", validation=validation, libre_device=libre_device)
assert result["success"] is False
assert "Site" in result["error"]
@patch("netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI")
def test_missing_device_type_returns_error(self, MockAPI):
from netbox_librenms_plugin.import_utils.device_operations import import_single_device
libre_device = self._make_libre_device()
validation = {
"existing_device": None,
"site": {"found": True, "site": MagicMock()},
"device_type": {"matched": False, "device_type": None},
"device_role": {"found": True, "role": MagicMock()},
"platform": {"found": False, "platform": None},
"rack": {"rack": None},
}
with (
patch("netbox_librenms_plugin.import_utils.device_operations.Site"),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType"),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole"),
patch("netbox_librenms_plugin.import_utils.device_operations.Rack"),
):
result = import_single_device(1, server_key="default", validation=validation, libre_device=libre_device)
assert result["success"] is False
assert "device type" in result["error"].lower()
@patch("netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI")
def test_missing_device_role_returns_error(self, MockAPI):
from netbox_librenms_plugin.import_utils.device_operations import import_single_device
libre_device = self._make_libre_device()
validation = {
"existing_device": None,
"site": {"found": True, "site": MagicMock()},
"device_type": {"matched": True, "device_type": MagicMock()},
"device_role": {"found": False, "role": None},
"platform": {"found": False, "platform": None},
"rack": {"rack": None},
}
with (
patch("netbox_librenms_plugin.import_utils.device_operations.Site"),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType"),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole"),
patch("netbox_librenms_plugin.import_utils.device_operations.Rack"),
):
result = import_single_device(1, server_key="default", validation=validation, libre_device=libre_device)
assert result["success"] is False
assert "role" in result["error"].lower()
class TestValidateDeviceForImportEdgeCases:
"""Additional edge case tests to cover missing lines."""
def _make_api(self):
api = MagicMock()
api.server_key = "default"
api.cache_timeout = 300
return api
def _start_patches(self, extra_patches=None):
mock_device = MagicMock()
mock_device.objects.filter.return_value.first.return_value = None
mock_device.objects.filter.return_value.exclude.return_value.first.return_value = None
mock_device.objects.all.return_value = []
mock_vm = MagicMock()
mock_vm.objects.filter.return_value.first.return_value = None
mock_cluster = MagicMock()
mock_cluster.objects.all.return_value = []
mock_role = MagicMock()
mock_role.objects.all.return_value = []
mock_ip = MagicMock()
mock_ip.objects.filter.return_value.first.return_value = None
mock_site = MagicMock()
mock_site.objects.all.return_value = []
patches = [
patch(
"netbox_librenms_plugin.import_utils.device_operations.find_matching_site",
return_value={"found": False, "site": None, "match_type": None, "suggestions": []},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
return_value={"matched": False, "device_type": None, "match_type": None},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.find_matching_platform",
return_value={"found": False, "platform": None, "match_type": None},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data",
return_value={"is_stack": False, "member_count": 0, "members": []},
),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole", mock_role),
patch("netbox_librenms_plugin.import_utils.device_operations.Cluster", mock_cluster),
patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType", MagicMock()),
patch("netbox_librenms_plugin.import_utils.device_operations.Site", mock_site),
patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id", return_value=None),
patch("netbox_librenms_plugin.import_utils.device_operations.cache"),
patch("virtualization.models.VirtualMachine", mock_vm),
patch("ipam.models.IPAddress", mock_ip),
]
if extra_patches:
patches.extend(extra_patches)
started = []
for p in patches:
started.append(p.start())
return patches, started
def _stop_patches(self, patches):
for p in reversed(patches):
p.stop()
def test_vm_librenms_id_not_int_falls_back(self):
"""Lines 288-290: librenms_id ValueError/TypeError in VM check."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
libre_device = {
"device_id": None,
"hostname": "vm01",
"sysName": "vm01",
"hardware": "-",
"serial": "-",
"os": "-",
"location": "-",
}
api = self._make_api()
patches, _ = self._start_patches()
try:
result = validate_device_for_import(libre_device, api=api)
finally:
self._stop_patches(patches)
assert result is not None
def test_vm_with_legacy_librenms_id_flags_migration(self):
"""Line 307: existing VM has legacy bare-int librenms_id → flags migration."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
libre_device = {
"device_id": 42,
"hostname": "vm01",
"sysName": "vm01",
"hardware": "-",
"serial": "-",
"os": "-",
"location": "-",
}
api = self._make_api()
existing_vm = MagicMock()
existing_vm.name = "vm01"
existing_vm.serial = ""
existing_vm.custom_field_data = {"librenms_id": 42} # Legacy bare-int
patches, _ = self._start_patches()
try:
with patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id") as mock_find:
mock_find.side_effect = [existing_vm, None] # VM found, then no Device
result = validate_device_for_import(libre_device, import_as_vm=True, api=api)
finally:
self._stop_patches(patches)
assert result.get("existing_device") is existing_vm
assert result.get("librenms_id_needs_migration") is True
def test_vc_detection_called_for_device_with_api(self):
"""Lines 616-638: VC detection executed when include_vc_detection=True and api provided."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
libre_device = {
"device_id": 1,
"hostname": "sw01",
"sysName": "sw01",
"hardware": "Cisco",
"serial": "SN001",
"os": "ios",
"location": "-",
}
api = self._make_api()
vc_data = {"is_stack": True, "member_count": 2, "members": [{"serial": "SN001"}, {"serial": "SN002"}]}
patches, _ = self._start_patches(
[
patch(
"netbox_librenms_plugin.import_utils.device_operations.update_vc_member_suggested_names",
return_value=vc_data,
),
]
)
# Override get_virtual_chassis_data to return VC stack
try:
with patch(
"netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data", return_value=vc_data
) as mock_get_vc:
with patch(
"netbox_librenms_plugin.import_utils.device_operations.update_vc_member_suggested_names",
return_value=vc_data,
) as mock_update_vc:
result = validate_device_for_import(libre_device, api=api)
finally:
self._stop_patches(patches)
assert result["virtual_chassis"] is not None
mock_get_vc.assert_called_once()
mock_update_vc.assert_called_once()
def test_no_vc_detection_when_disabled(self):
"""VC detection skipped when include_vc_detection=False."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
libre_device = {
"device_id": 1,
"hostname": "sw01",
"sysName": "sw01",
"hardware": "-",
"serial": "-",
"os": "-",
"location": "-",
}
api = self._make_api()
patches, _ = self._start_patches()
try:
with patch("netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data") as mock_vc:
validate_device_for_import(libre_device, api=api, include_vc_detection=False)
mock_vc.assert_not_called()
finally:
self._stop_patches(patches)
def test_chassis_inventory_fallback_used(self):
"""Lines 534-539: Chassis inventory fallback when hardware doesn't match."""
from netbox_librenms_plugin.import_utils.device_operations import _try_chassis_device_type_match
api = MagicMock()
mock_dt = MagicMock()
api.get_inventory_filtered.return_value = (
True,
[{"entPhysicalName": "MX480", "entPhysicalModelName": "Juniper MX480"}],
)
with patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type"
) as mock_match:
mock_match.side_effect = [
{"matched": False},
{"matched": True, "device_type": mock_dt, "match_type": "exact"},
]
result = _try_chassis_device_type_match(api, 1)
# Should have found a match via model name fallback
assert result is not None
assert mock_match.call_count == 2
assert result["matched"] is True
assert result.get("device_type") is mock_dt
def test_primary_ip_match_check(self):
"""Lines 474-489: IP address match detection."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
libre_device = {
"device_id": 1,
"hostname": "router01",
"sysName": "router01",
"hardware": "-",
"serial": "-",
"os": "-",
"location": "-",
"ip": "192.168.1.1",
}
api = self._make_api()
mock_device = MagicMock()
mock_device.name = "existing_router"
mock_ip = MagicMock()
mock_ip.assigned_object.device = mock_device
mock_ip_model = MagicMock()
mock_ip_model.objects.filter.return_value.first.return_value = mock_ip
patches, _ = self._start_patches()
try:
with patch("ipam.models.IPAddress", mock_ip_model):
result = validate_device_for_import(libre_device, api=api)
finally:
self._stop_patches(patches)
# The IP match should set existing_device to the mock device and match_type to "primary_ip"
assert result.get("existing_device") is mock_device
assert result.get("existing_match_type") == "primary_ip"
def test_no_hostname_adds_issue(self):
"""When hostname and sysName are both empty, _determine_device_name falls back to device-{id}."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
libre_device = {
"device_id": 1,
"hostname": "",
"sysName": "",
"hardware": "-",
"serial": "-",
"os": "-",
"location": "-",
}
api = self._make_api()
patches, _ = self._start_patches()
try:
result = validate_device_for_import(libre_device, api=api)
finally:
self._stop_patches(patches)
# _determine_device_name always falls back to "device-{id}" so
# "Device has no hostname" issue is not expected here.
assert isinstance(result, dict)
assert "Device has no hostname" not in result.get("issues", [])
assert result.get("resolved_name", "").startswith("device-")
class TestValidateDeviceMoreEdgeCases:
"""More edge case tests for validate_device_for_import."""
def _make_api(self):
api = MagicMock()
api.server_key = "default"
api.cache_timeout = 300
return api
def _get_patches(self):
mock_device = MagicMock()
mock_device.objects.filter.return_value.first.return_value = None
mock_device.objects.filter.return_value.exclude.return_value.first.return_value = None
mock_device.objects.all.return_value = []
mock_vm = MagicMock()
mock_vm.objects.filter.return_value.first.return_value = None
mock_site = MagicMock()
mock_site.objects.all.return_value = []
mock_cluster = MagicMock()
mock_cluster.objects.all.return_value = []
mock_role = MagicMock()
mock_role.objects.all.return_value = []
mock_ip = MagicMock()
mock_ip.objects.filter.return_value.first.return_value = None
return [
patch(
"netbox_librenms_plugin.import_utils.device_operations.find_matching_site",
return_value={"found": False, "site": None, "match_type": None, "suggestions": []},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
return_value={"matched": False, "device_type": None, "match_type": None},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.find_matching_platform",
return_value={"found": False, "platform": None, "match_type": None},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data",
return_value={"is_stack": False, "member_count": 0, "members": []},
),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole", mock_role),
patch("netbox_librenms_plugin.import_utils.device_operations.Cluster", mock_cluster),
patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType", MagicMock()),
patch("netbox_librenms_plugin.import_utils.device_operations.Site", mock_site),
patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id", return_value=None),
patch("netbox_librenms_plugin.import_utils.device_operations.cache"),
patch("virtualization.models.VirtualMachine", mock_vm),
patch("ipam.models.IPAddress", mock_ip),
]
def test_serial_dash_normalized(self):
"""Line 346: serial '-' is normalized to empty string."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
existing = MagicMock()
existing.name = "router01"
existing.serial = "SN001"
existing.custom_field_data = {"librenms_id": {"default": 1}}
existing.virtual_chassis = MagicMock() # Has VC
existing.vc_position = 1
libre_device = {
"device_id": 1,
"hostname": "router01",
"sysName": "router01",
"hardware": "-",
"serial": "-", # Dash serial
"os": "-",
"location": "",
}
api = self._make_api()
patches = self._get_patches()
try:
for p in patches:
p.start()
# Return None for VM, existing for Device
def _device_side_effect(model, device_id, server_key):
from virtualization.models import VirtualMachine as VM
return None if model is VM else existing
with patch(
"netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id",
side_effect=_device_side_effect,
):
result = validate_device_for_import(libre_device, api=api)
finally:
for p in patches:
p.stop()
assert result is not None
# serial '-' must be treated as empty — no serial mismatch should be flagged
assert result.get("serial_action") is None
def test_serial_conflict_with_existing_device(self):
"""Lines 373-375: incoming serial already used by another device."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
existing = MagicMock()
existing.name = "router01"
existing.serial = "OLD_SN" # Different from incoming
existing.custom_field_data = {"librenms_id": {"default": 1}}
existing.virtual_chassis = None
conflict_device = MagicMock()
conflict_device.name = "router02"
conflict_device.pk = 99
libre_device = {
"device_id": 1,
"hostname": "router01",
"sysName": "router01",
"hardware": "-",
"serial": "NEW_SN", # Different serial
"os": "-",
"location": "",
}
api = self._make_api()
mock_device = MagicMock()
mock_device.objects.filter.return_value.first.return_value = None
mock_device.objects.filter.return_value.exclude.return_value.first.return_value = conflict_device
patches = self._get_patches()
try:
for p in patches:
p.start()
# Return None for VM check, existing for Device check
def _find_side_effect(model, device_id, server_key):
from virtualization.models import VirtualMachine as VM
if model is VM:
return None
return existing
with (
patch(
"netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id",
side_effect=_find_side_effect,
),
patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
):
result = validate_device_for_import(libre_device, api=api)
finally:
for p in patches:
p.stop()
assert result.get("serial_action") == "conflict"
def test_both_vm_and_device_with_same_hostname(self):
"""Lines 395-399: both VM and Device have same hostname - ambiguous match."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
libre_device = {
"device_id": 1,
"hostname": "server01",
"sysName": "server01",
"hardware": "-",
"serial": "-",
"os": "-",
"location": "",
}
api = self._make_api()
existing_vm = MagicMock()
existing_vm.name = "server01"
existing_device = MagicMock()
existing_device.name = "server01"
mock_vm = MagicMock()
mock_vm.objects.filter.return_value.first.return_value = existing_vm
mock_device = MagicMock()
mock_device.objects.filter.return_value.first.return_value = existing_device
mock_device.objects.filter.return_value.exclude.return_value.first.return_value = None
mock_device.objects.all.return_value = []
mock_site = MagicMock()
mock_site.objects.all.return_value = []
patches = self._get_patches()
try:
for p in patches:
p.start()
with (
patch("virtualization.models.VirtualMachine", mock_vm),
patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
patch("netbox_librenms_plugin.import_utils.device_operations.Site", mock_site),
):
result = validate_device_for_import(libre_device, api=api)
finally:
for p in patches:
p.stop()
# Ambiguous - should have a warning about both existing
assert result is not None
assert any("VM" in w and "Device" in w for w in result.get("warnings", []))
def test_existing_vm_by_hostname(self):
"""Lines 406-413: VM found by hostname (no Device match)."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
libre_device = {
"device_id": 1,
"hostname": "vm01",
"sysName": "vm01",
"hardware": "-",
"serial": "-",
"os": "-",
"location": "",
}
api = self._make_api()
existing_vm = MagicMock()
existing_vm.name = "vm01"
existing_vm.custom_field_data = {}
mock_vm = MagicMock()
mock_vm.objects.filter.return_value.first.return_value = existing_vm # VM found
mock_device = MagicMock()
mock_device.objects.filter.return_value.first.return_value = None # No device match
mock_device.objects.filter.return_value.exclude.return_value.first.return_value = None
mock_device.objects.all.return_value = []
patches = self._get_patches()
try:
for p in patches:
p.start()
with (
patch("virtualization.models.VirtualMachine", mock_vm),
patch("netbox_librenms_plugin.import_utils.device_operations.Device", mock_device),
):
result = validate_device_for_import(libre_device, api=api)
finally:
for p in patches:
p.stop()
assert result.get("existing_device") is existing_vm
assert result.get("existing_match_type") == "hostname"
def test_vc_detection_exception_handled(self):
"""Lines 634-636: VC detection exception is caught and stored."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
libre_device = {
"device_id": 1,
"hostname": "sw01",
"sysName": "sw01",
"hardware": "-",
"serial": "-",
"os": "-",
"location": "",
}
api = self._make_api()
patches = self._get_patches()
try:
for p in patches:
p.start()
with patch(
"netbox_librenms_plugin.import_utils.device_operations.get_virtual_chassis_data",
side_effect=Exception("VC error"),
):
result = validate_device_for_import(libre_device, api=api)
finally:
for p in patches:
p.stop()
assert result is not None
assert "detection_error" in result.get("virtual_chassis", {})
class TestImportSingleDeviceEdgeCases:
"""Tests for import_single_device edge cases (lines 737-739, 777-789)."""
@patch("netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI")
def test_no_libre_device_api_failure(self, MockAPI):
"""Lines 737-739: libre_device=None and API fails → returns error dict."""
from netbox_librenms_plugin.import_utils.device_operations import import_single_device
mock_api = MagicMock()
mock_api.server_key = "default"
mock_api.get_device_info.return_value = (False, None)
MockAPI.return_value = mock_api
result = import_single_device(device_id=1, libre_device=None, server_key="default")
assert result["success"] is False
assert "Failed to retrieve device" in result.get("error", "")
@patch("netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI")
def test_manual_mappings_are_applied(self, MockAPI):
"""Lines 777-789: manual_mappings override site/device_type/device_role."""
from netbox_librenms_plugin.import_utils.device_operations import import_single_device
mock_api = MagicMock()
mock_api.server_key = "default"
MockAPI.return_value = mock_api
libre_device = {
"device_id": 1,
"hostname": "router01",
"hardware": "Cisco",
"serial": "SN001",
"os": "ios",
"location": "",
}
validation = {
"is_ready": True,
"can_import": True,
"existing_device": None,
"import_as_vm": False,
"site": {"found": True, "site": None},
"device_type": {"found": True, "device_type": None},
"device_role": {"found": False, "role": None},
"platform": {"found": False, "platform": None},
"rack": {"rack": None},
"issues": [],
}
mock_site = MagicMock()
mock_site.pk = 1
mock_dt = MagicMock()
mock_dt.pk = 1
mock_role = MagicMock()
mock_role.pk = 1
manual_mappings = {"site_id": 1, "device_type_id": 1, "device_role_id": 1}
mock_tx = MagicMock()
mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None)
mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False)
with patch("netbox_librenms_plugin.import_utils.device_operations.transaction", mock_tx):
with patch("netbox_librenms_plugin.import_utils.device_operations.Site") as mock_site_cls:
mock_site_cls.objects.filter.return_value.first.return_value = mock_site
with patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType") as mock_dt_cls:
mock_dt_cls.objects.filter.return_value.first.return_value = mock_dt
with patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole") as mock_role_cls:
mock_role_cls.objects.filter.return_value.first.return_value = mock_role
with patch("netbox_librenms_plugin.import_utils.device_operations.Rack") as mock_rack_cls:
mock_rack_cls.objects.select_related.return_value.filter.return_value.first.return_value = (
None
)
with patch(
"netbox_librenms_plugin.import_utils.device_operations.Device"
) as mock_device_cls:
mock_device_cls.objects.filter.return_value.first.return_value = None
mock_new_device = MagicMock()
mock_device_cls.return_value = mock_new_device
mock_new_device.full_clean.return_value = None
mock_new_device.save.return_value = None
mock_new_device.pk = 99
with patch(
"netbox_librenms_plugin.import_utils.device_operations.set_librenms_device_id"
) as mock_set_id:
with patch(
"netbox_librenms_plugin.import_utils.device_operations.validate_device_for_import",
return_value=validation,
):
with patch(
"netbox_librenms_plugin.import_utils.device_operations.timezone"
) as mock_tz:
mock_tz.now.return_value.strftime.return_value = "2024-01-01 00:00:00 UTC"
result = import_single_device(
device_id=1,
libre_device=libre_device,
validation=validation,
manual_mappings=manual_mappings,
server_key="default",
)
# Should have succeeded
assert result.get("success") is True
mock_new_device.full_clean.assert_called_once()
mock_new_device.save.assert_called_once()
mock_set_id.assert_called_once()
class TestImportSingleDeviceMoreEdgeCases:
"""Tests for device_operations additional coverage (lines 539, 783-785, 789)."""
def _make_api(self):
api = MagicMock()
api.server_key = "default"
api.cache_timeout = 300
return api
def _base_validation(self):
return {
"is_ready": True,
"can_import": True,
"existing_device": None,
"import_as_vm": False,
"site": {"found": True, "site": MagicMock()},
"device_type": {"found": True, "device_type": MagicMock()},
"device_role": {"found": True, "role": MagicMock()},
"platform": {"found": False, "platform": None},
"rack": {"rack": None},
"issues": [],
}
def _mock_tx(self):
mock_tx = MagicMock()
mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None)
mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False)
return mock_tx
def test_platform_manual_mapping(self):
"""Lines 783-785: manual_mappings with platform_id applied."""
from netbox_librenms_plugin.import_utils.device_operations import import_single_device
libre_device = {"device_id": 1, "hostname": "r01", "serial": "-", "hardware": "-", "os": "-", "location": ""}
validation = self._base_validation()
manual_mappings = {"platform_id": 3}
mock_platform = MagicMock()
mock_new_device = MagicMock()
mock_new_device.full_clean.return_value = None
mock_new_device.save.return_value = None
mock_new_device.pk = 10
with patch("netbox_librenms_plugin.import_utils.device_operations.transaction", self._mock_tx()):
with patch("netbox_librenms_plugin.import_utils.device_operations.Site") as MockSite:
MockSite.objects.filter.return_value.first.return_value = MagicMock()
with patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType") as MockDT:
MockDT.objects.filter.return_value.first.return_value = MagicMock()
with patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole") as MockRole:
MockRole.objects.filter.return_value.first.return_value = MagicMock()
with patch("netbox_librenms_plugin.import_utils.device_operations.Device") as MockDevice:
MockDevice.objects.filter.return_value.first.return_value = None
MockDevice.return_value = mock_new_device
with patch("dcim.models.Platform") as MockPlatform:
MockPlatform.objects.filter.return_value.first.return_value = mock_platform
with patch(
"netbox_librenms_plugin.import_utils.device_operations.set_librenms_device_id"
):
with patch(
"netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI"
) as MockAPI:
MockAPI.return_value = self._make_api()
with patch(
"netbox_librenms_plugin.import_utils.device_operations.timezone"
) as mock_tz:
mock_tz.now.return_value.strftime.return_value = "2024-01-01"
result = import_single_device(
device_id=1,
libre_device=libre_device,
validation=validation,
manual_mappings=manual_mappings,
server_key="default",
)
assert result.get("success") is True
# Verify that the platform was looked up with the correct ID and passed to Device()
MockPlatform.objects.filter.assert_called_with(id=manual_mappings["platform_id"])
assert MockDevice.call_args.kwargs.get("platform") is mock_platform
def test_rack_manual_mapping(self):
"""Line 789: manual_mappings with rack_id applied."""
from netbox_librenms_plugin.import_utils.device_operations import import_single_device
libre_device = {"device_id": 1, "hostname": "r01", "serial": "-", "hardware": "-", "os": "-", "location": ""}
validation = self._base_validation()
manual_mappings = {"rack_id": 5}
mock_rack = MagicMock()
mock_new_device = MagicMock()
mock_new_device.full_clean.return_value = None
mock_new_device.save.return_value = None
mock_new_device.pk = 10
with patch("netbox_librenms_plugin.import_utils.device_operations.transaction", self._mock_tx()):
with patch("netbox_librenms_plugin.import_utils.device_operations.Site") as MockSite:
MockSite.objects.filter.return_value.first.return_value = MagicMock()
with patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType") as MockDT:
MockDT.objects.filter.return_value.first.return_value = MagicMock()
with patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole") as MockRole:
MockRole.objects.filter.return_value.first.return_value = MagicMock()
with patch("netbox_librenms_plugin.import_utils.device_operations.Device") as MockDevice:
MockDevice.objects.filter.return_value.first.return_value = None
MockDevice.return_value = mock_new_device
with patch("netbox_librenms_plugin.import_utils.device_operations.Rack") as MockRack:
MockRack.objects.select_related.return_value.filter.return_value.first.return_value = (
mock_rack
)
with patch(
"netbox_librenms_plugin.import_utils.device_operations.set_librenms_device_id"
):
with patch(
"netbox_librenms_plugin.import_utils.device_operations.LibreNMSAPI"
) as MockAPI:
MockAPI.return_value = self._make_api()
with patch(
"netbox_librenms_plugin.import_utils.device_operations.timezone"
) as mock_tz:
mock_tz.now.return_value.strftime.return_value = "2024-01-01"
result = import_single_device(
device_id=1,
libre_device=libre_device,
validation=validation,
manual_mappings=manual_mappings,
server_key="default",
)
assert result.get("success") is True
# Verify that the rack was looked up with the correct ID and passed to Device()
MockRack.objects.select_related.return_value.filter.assert_called_with(id=manual_mappings["rack_id"])
assert MockDevice.call_args.kwargs.get("rack") is mock_rack
class TestValidateDeviceExistingVMGuard:
"""Test that existing VMs skip device-specific validations (g06 fix)."""
def _make_api(self):
api = MagicMock()
api.server_key = "default"
api.cache_timeout = 300
return api
def test_existing_vm_skips_device_validations(self):
"""
When import_as_vm=True and existing_device is set, site/device_type/device_role
are marked found=True without running device-specific validation logic."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
existing_vm = MagicMock()
existing_vm.name = "vm01"
existing_vm.custom_field_data = {}
libre_device = {
"device_id": 1,
"hostname": "vm01",
"sysName": "vm01",
"hardware": "-",
"serial": "-",
"os": "-",
"location": "unknown-location",
}
api = self._make_api()
mock_vm_model = MagicMock()
mock_vm_model.objects.filter.return_value.first.return_value = existing_vm
mock_device_model = MagicMock()
mock_device_model.objects.filter.return_value.first.return_value = None
mock_device_model.objects.filter.return_value.exclude.return_value.first.return_value = None
mock_device_model.objects.all.return_value = []
with (
patch("netbox_librenms_plugin.import_utils.device_operations.Site"),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType"),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole"),
patch("netbox_librenms_plugin.import_utils.device_operations.Device", new=mock_device_model),
patch("netbox_librenms_plugin.import_utils.device_operations.Cluster"),
patch("netbox_librenms_plugin.import_utils.device_operations.cache"),
patch("virtualization.models.VirtualMachine", new=mock_vm_model),
patch("ipam.models.IPAddress"),
patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id", return_value=None),
patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type"
) as mock_match,
patch("netbox_librenms_plugin.import_utils.device_operations.find_matching_site") as mock_site,
):
result = validate_device_for_import(libre_device, import_as_vm=True, api=api)
# find_matching_site and match_librenms_hardware_to_device_type should NOT be called for VMs
mock_site.assert_not_called()
mock_match.assert_not_called()
# Device-specific fields are marked found=True for all VMs
assert result["site"]["found"] is True
assert result["device_type"]["found"] is True
assert result["device_role"]["found"] is True
# No cluster-required error for existing VMs
assert not any("Cluster must be" in i for i in result.get("issues", []))
class TestValidateDeviceChassisMatch:
"""Test chassis match path (line 539) in validate_device_for_import."""
def _make_api(self):
api = MagicMock()
api.server_key = "default"
return api
def test_chassis_match_overrides_hardware_match(self):
"""Line 539: chassis_match succeeds → dt_match = chassis_match."""
from netbox_librenms_plugin.import_utils.device_operations import validate_device_for_import
libre_device = {
"device_id": 1,
"hostname": "sw01",
"sysName": "sw01",
"hardware": "Cisco Catalyst 9300",
"serial": "SN001",
"os": "ios",
"location": "",
}
api = self._make_api()
chassis_dt = MagicMock()
chassis_dt.model = "Catalyst 9300"
chassis_match = {"matched": True, "device_type": chassis_dt, "match_type": "chassis_inventory"}
vm_no_match = MagicMock()
vm_no_match.objects.filter.return_value.first.return_value = None # no hostname collision
device_patch = patch("netbox_librenms_plugin.import_utils.device_operations.Device")
mock_device_cls = device_patch.start()
mock_device_cls.objects.filter.return_value.first.return_value = None
mock_device_cls.objects.filter.return_value.exclude.return_value.first.return_value = None
patches = [
patch("netbox_librenms_plugin.import_utils.device_operations.Site"),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceType"),
patch("netbox_librenms_plugin.import_utils.device_operations.DeviceRole"),
patch("netbox_librenms_plugin.import_utils.device_operations.cache"),
patch("virtualization.models.VirtualMachine", new=vm_no_match),
patch("ipam.models.IPAddress"),
patch("netbox_librenms_plugin.import_utils.device_operations.find_by_librenms_id", return_value=None),
patch(
"netbox_librenms_plugin.import_utils.device_operations.match_librenms_hardware_to_device_type",
return_value={"matched": False},
),
patch(
"netbox_librenms_plugin.import_utils.device_operations._try_chassis_device_type_match",
return_value=chassis_match,
),
patch(
"netbox_librenms_plugin.import_utils.device_operations.find_matching_platform",
return_value={"found": False, "platform": None, "match_type": None},
),
]
for p in patches:
p.start()
try:
result = validate_device_for_import(libre_device, api=api)
finally:
for p in patches:
p.stop()
device_patch.stop()
assert result["device_type"].get("device_type") is chassis_dt