""" Comprehensive tests for LibreNMSAPI client. This module provides 100% test coverage for netbox_librenms_plugin/librenms_api.py, with particular focus on HTTP method correctness to prevent regression bugs. """ from unittest.mock import MagicMock, patch import pytest import requests # Import the autouse fixture from helpers pytest_plugins = ["netbox_librenms_plugin.tests.test_librenms_api_helpers"] # ============================================================================= # Test Class 1: Initialization (3 tests) # ============================================================================= class TestLibreNMSAPIInit: """Test LibreNMSAPI initialization and configuration loading.""" def test_init_with_multi_server_config(self, mock_librenms_config): """Verify initialization with multi-server configuration.""" from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") assert api.librenms_url == "https://librenms.example.com" assert api.api_token == "test-token" assert api.cache_timeout == 300 assert api.verify_ssl is True def test_init_with_legacy_config(self, mock_librenms_config): """Verify initialization with legacy single-server config.""" mock_config = mock_librenms_config["mock_config"] # Return None for 'servers' to trigger legacy path def config_side_effect(plugin, key, default=None): if key == "servers": return None legacy = { "librenms_url": "https://legacy.example.com", "api_token": "legacy-token", "cache_timeout": 600, "verify_ssl": False, } return legacy.get(key, default) mock_config.side_effect = config_side_effect from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI() assert api.librenms_url == "https://legacy.example.com" def test_init_missing_config_raises_valueerror(self, mock_librenms_config): """Verify ValueError raised when configuration is missing.""" mock_config = mock_librenms_config["mock_config"] mock_config.return_value = None from netbox_librenms_plugin.librenms_api import LibreNMSAPI with pytest.raises(ValueError): LibreNMSAPI(server_key="nonexistent") def test_init_nonexistent_server_key_raises_keyerror(self, mock_librenms_config): """Verify KeyError raised when specific server_key doesn't exist.""" from netbox_librenms_plugin.librenms_api import LibreNMSAPI with pytest.raises(KeyError, match="nonexistent"): LibreNMSAPI(server_key="nonexistent") def test_init_default_falls_back_to_first_server(self, mock_librenms_config): """Verify 'default' key falls back to first configured server.""" mock_config = mock_librenms_config["mock_config"] mock_config.return_value = { "primary": { "librenms_url": "https://primary.example.com", "api_token": "primary-token", } } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") assert api.server_key == "primary" assert api.librenms_url == "https://primary.example.com" # ============================================================================= # Test Class 2: Connection Testing (4 tests) # ============================================================================= class TestLibreNMSAPIConnection: """Test connection testing functionality.""" @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_connection_success(self, mock_get, mock_librenms_config): """Verify successful connection test.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "system": [{"local_ver": "24.1.0"}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") result = api.test_connection() assert result is not None assert "error" not in result mock_get.assert_called_once() assert "/api/v0/system" in mock_get.call_args[0][0] @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_connection_auth_failure_401(self, mock_get, mock_librenms_config): """Verify 401 unauthorized handling.""" mock_get.return_value.status_code = 401 from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") result = api.test_connection() assert result.get("error") is True @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_connection_auth_failure_403(self, mock_get, mock_librenms_config): """Verify 403 forbidden handling.""" mock_get.return_value.status_code = 403 from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") result = api.test_connection() assert result.get("error") is True @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_connection_timeout(self, mock_get, mock_librenms_config): """Verify timeout exception handling.""" mock_get.side_effect = requests.exceptions.Timeout("Connection timed out") from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") result = api.test_connection() assert result.get("error") is True assert "timeout" in result.get("message", "").lower() # ============================================================================= # Test Class 3: HTTP Methods - CRITICAL (18 tests) # ============================================================================= class TestLibreNMSAPIHttpMethods: """ CRITICAL: Verify each API method uses the correct HTTP verb. These tests prevent regression bugs where HTTP methods are accidentally changed during refactoring (e.g., GET changed to DELETE). """ @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.post") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_info_uses_get(self, mock_get, mock_post, mock_delete, mock_librenms_config): """Verify get_device_info uses GET, never DELETE.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "devices": [{"device_id": 1}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.get_device_info(device_id=1) mock_get.assert_called_once() mock_delete.assert_not_called() mock_post.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.get") @patch("netbox_librenms_plugin.librenms_api.requests.post") def test_add_device_uses_post(self, mock_post, mock_get, mock_delete, mock_librenms_config): """Verify add_device uses POST.""" mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = {"status": "ok", "device_id": 42} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.add_device( data={ "hostname": "test.example.com", "snmp_version": "v2c", "community": "public", } ) mock_post.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.patch") def test_update_device_field_uses_patch(self, mock_patch, mock_delete, mock_librenms_config): """Verify update_device_field uses PATCH.""" mock_patch.return_value.status_code = 200 mock_patch.return_value.json.return_value = {"status": "ok"} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.update_device_field(device_id=1, field_data={"field": ["location"], "data": ["DC1"]}) mock_patch.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_id_by_ip_uses_get(self, mock_get, mock_delete, mock_librenms_config): """Verify get_device_id_by_ip uses GET.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "devices": [{"device_id": 10}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.get_device_id_by_ip("192.168.1.1") mock_get.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_id_by_hostname_uses_get(self, mock_get, mock_delete, mock_librenms_config): """Verify get_device_id_by_hostname uses GET.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "devices": [{"device_id": 20}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.get_device_id_by_hostname("test-host") mock_get.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_ports_uses_get(self, mock_get, mock_delete, mock_librenms_config): """Verify get_ports uses GET.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok", "ports": []} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.get_ports(device_id=1) mock_get.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_locations_uses_get(self, mock_get, mock_delete, mock_librenms_config): """Verify get_locations uses GET.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok", "locations": []} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.get_locations() mock_get.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.post") def test_add_location_uses_post(self, mock_post, mock_delete, mock_librenms_config): """Verify add_location uses POST.""" mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = { "status": "ok", "message": "Location created #1", } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.add_location(location_data={"location": "DC1"}) mock_post.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.patch") def test_update_location_uses_patch(self, mock_patch, mock_delete, mock_librenms_config): """Verify update_location uses PATCH.""" mock_patch.return_value.status_code = 200 mock_patch.return_value.json.return_value = { "status": "ok", "message": "Location updated", } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.update_location(location_name="DC1", location_data={"location": "DC1-Updated"}) mock_patch.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_links_uses_get(self, mock_get, mock_delete, mock_librenms_config): """Verify get_device_links uses GET.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok", "links": []} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.get_device_links(device_id=1) mock_get.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_ips_uses_get(self, mock_get, mock_delete, mock_librenms_config): """Verify get_device_ips uses GET.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok", "addresses": []} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.get_device_ips(device_id=1) mock_get.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_port_by_id_uses_get(self, mock_get, mock_delete, mock_librenms_config): """Verify get_port_by_id uses GET.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok", "port": [{}]} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.get_port_by_id(port_id=1) mock_get.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_inventory_uses_get(self, mock_get, mock_delete, mock_librenms_config): """Verify get_device_inventory uses GET.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok", "inventory": []} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.get_device_inventory(device_id=1) mock_get.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_poller_groups_uses_get(self, mock_get, mock_delete, mock_librenms_config): """Verify get_poller_groups uses GET.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok", "groups": []} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.get_poller_groups() mock_get.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_inventory_filtered_uses_get(self, mock_get, mock_delete, mock_librenms_config): """Verify get_inventory_filtered uses GET.""" mock_get.return_value.status_code = 200 # Return non-empty inventory so it doesn't fall back to get_device_inventory mock_get.return_value.json.return_value = { "status": "ok", "inventory": [{"entPhysicalClass": "chassis"}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.get_inventory_filtered(device_id=1, ent_physical_class="chassis") mock_get.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.delete") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_list_devices_uses_get(self, mock_get, mock_delete, mock_librenms_config): """Verify list_devices uses GET.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok", "devices": []} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.list_devices() mock_get.assert_called_once() mock_delete.assert_not_called() @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_test_connection_uses_get(self, mock_get, mock_librenms_config): """Verify test_connection uses GET.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok"} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") api.test_connection() mock_get.assert_called_once() # ==================================================================================== # Test Class 4: Device Lookup (6 tests) # ==================================================================================== class TestLibreNMSAPIDeviceLookup: """Test device lookup functionality.""" def test_get_librenms_id_from_custom_field(self, mock_librenms_config): """Returns ID when already stored in cf['librenms_id'].""" from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") device = MagicMock() device.name = "test-device" device.cf = {"librenms_id": 42} result = api.get_librenms_id(device) assert result == 42 def test_get_librenms_id_normalizes_string_to_int(self, mock_librenms_config): """Returns int for string-stored librenms_id; read path uses auto_save=False so no write-back.""" from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") device = MagicMock() device.name = "test-device" device.cf = {"librenms_id": "42"} device.custom_field_data = {"librenms_id": "42"} result = api.get_librenms_id(device) assert result == 42 # auto_save=False in this read path — normalization is NOT written back device.save.assert_not_called() def test_get_librenms_id_empty_string_falls_through_to_discovery(self, mock_librenms_config): """An empty-string librenms_id is treated as not set (falls through to API discovery).""" from unittest.mock import patch from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") device = MagicMock() device.name = "test-device" device.cf = {"librenms_id": ""} device.primary_ip = None with patch.object(api, "get_device_id_by_hostname", return_value=None): result = api.get_librenms_id(device) assert result is None @patch("netbox_librenms_plugin.librenms_api.cache") def test_get_librenms_id_from_cache(self, mock_cache, mock_librenms_config): """Returns ID from Django cache when not in custom field.""" mock_cache.get.return_value = 99 from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") device = MagicMock() device.name = "test-device" device.cf = {} result = api.get_librenms_id(device) assert result == 99 @patch("netbox_librenms_plugin.librenms_api.cache") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_librenms_id_by_ip_lookup(self, mock_get, mock_cache, mock_librenms_config): """Performs IP lookup and caches result.""" mock_cache.get.return_value = None mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "devices": [{"device_id": 55}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") device = MagicMock() device.name = "test-device" device.cf = {} device.primary_ip4 = MagicMock() device.primary_ip4.address = MagicMock() device.primary_ip4.address.ip = "10.0.0.1" result = api.get_librenms_id(device) assert result == 55 mock_cache.set.assert_called_once() @patch("netbox_librenms_plugin.librenms_api.cache") @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_librenms_id_by_hostname_lookup(self, mock_get, mock_cache, mock_librenms_config): """Falls back to hostname lookup.""" mock_cache.get.return_value = None # First call (IP lookup) returns empty, second call (hostname lookup) returns device call_count = [0] def side_effect(*args, **kwargs): resp = MagicMock() resp.status_code = 200 call_count[0] += 1 if call_count[0] == 2: # Second call is hostname lookup resp.json.return_value = { "status": "ok", "devices": [{"device_id": 77}], } else: resp.json.return_value = {"status": "ok", "devices": []} return resp mock_get.side_effect = side_effect from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") device = MagicMock() device.name = "test-device" device.cf = {} device.primary_ip4 = MagicMock() device.primary_ip4.address = MagicMock() device.primary_ip4.address.ip = "10.0.0.1" result = api.get_librenms_id(device) assert result == 77 @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_id_by_ip_not_found(self, mock_get, mock_librenms_config): """Returns None when IP not found in LibreNMS.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok", "devices": []} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") result = api.get_device_id_by_ip("192.168.99.99") assert result is None @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_id_by_hostname_not_found(self, mock_get, mock_librenms_config): """Returns None when hostname not found.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok", "devices": []} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") result = api.get_device_id_by_hostname("nonexistent-host") assert result is None # ============================================================================= # Test Class 5: Device Operations (6 tests) # ============================================================================= class TestLibreNMSAPIDeviceOperations: """Test device CRUD operations.""" pytest_plugins = ["netbox_librenms_plugin.tests.test_librenms_api_helpers"] @patch("netbox_librenms_plugin.librenms_api.requests.post") def test_add_device_success(self, mock_post, mock_librenms_config): """Verify successful device addition.""" mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = { "status": "ok", "message": "Device added successfully", } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") result = api.add_device( data={ "hostname": "test-device.example.com", "snmp_version": "v2c", "community": "public", } ) assert result[0] is True assert result[1] == "Device added successfully." @patch("netbox_librenms_plugin.librenms_api.requests.post") def test_add_device_snmpv1_success(self, mock_post, mock_librenms_config): """Verify successful device addition using SNMPv1.""" mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = { "status": "ok", "message": "Device added successfully", } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") result = api.add_device( data={ "hostname": "legacy-device.example.com", "snmp_version": "v1", "community": "public", } ) assert result[0] is True assert result[1] == "Device added successfully." # Verify the payload includes correct snmpver and community call_args = mock_post.call_args payload = call_args.kwargs.get("json") or call_args[1].get("json") assert payload["snmpver"] == "v1" assert payload["community"] == "public" @patch("netbox_librenms_plugin.librenms_api.requests.post") def test_add_device_duplicate_error(self, mock_post, mock_librenms_config): """Verify duplicate device handling.""" mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = { "status": "error", "message": "Device already exists", } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") result = api.add_device( data={ "hostname": "duplicate-device.example.com", "snmp_version": "v2c", "community": "public", } ) assert result[0] is False assert "Device already exists" in result[1] @patch("netbox_librenms_plugin.librenms_api.requests.post") def test_add_device_snmpv3_success(self, mock_post, mock_librenms_config): """Verify successful device addition using SNMPv3 with all required fields.""" mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = { "status": "ok", "message": "Device added successfully", } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") result = api.add_device( data={ "hostname": "secure-device.example.com", "snmp_version": "v3", "authlevel": "authPriv", "authname": "snmpuser", "authpass": "authpassword123", "authalgo": "SHA", "cryptopass": "cryptopassword456", "cryptoalgo": "AES", } ) assert result[0] is True assert result[1] == "Device added successfully." # Verify the payload includes correct snmpver and all v3 fields call_args = mock_post.call_args payload = call_args.kwargs.get("json") or call_args[1].get("json") assert payload["snmpver"] == "v3" assert payload["authlevel"] == "authPriv" assert payload["authname"] == "snmpuser" assert payload["authpass"] == "authpassword123" assert payload["authalgo"] == "SHA" assert payload["cryptopass"] == "cryptopassword456" assert payload["cryptoalgo"] == "AES" # Ensure community is NOT included for v3 assert "community" not in payload @patch("netbox_librenms_plugin.librenms_api.requests.patch") def test_update_device_field_success(self, mock_patch, mock_librenms_config): """Verify successful device field update.""" mock_patch.return_value.status_code = 200 mock_patch.return_value.json.return_value = { "status": "ok", "message": "Device field updated", } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, message = api.update_device_field(device_id=123, field_data={"field": "notes", "data": "Updated note"}) assert success is True assert "updated" in message.lower() @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_info_success(self, mock_get, mock_librenms_config): """Verify retrieving device info.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "devices": [{"device_id": 123, "hostname": "test-device"}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, device_data = api.get_device_info(device_id=123) assert success is True assert device_data is not None assert device_data["device_id"] == 123 @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_info_not_found(self, mock_get, mock_librenms_config): """Empty devices list returns (False, None) without raising.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok", "devices": []} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, result = api.get_device_info(1) assert success is False assert result is None @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_list_devices_with_filters(self, mock_get, mock_librenms_config): """Verify listing devices with filter parameter.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "devices": [{"device_id": 1}, {"device_id": 2}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, devices = api.list_devices(filters={"type": "network"}) assert success is True assert len(devices) == 2 # ============================================================================= # Test Class 6: Location Operations (4 tests) # ============================================================================= class TestLibreNMSAPILocationOperations: """Test location CRUD operations.""" pytest_plugins = ["netbox_librenms_plugin.tests.test_librenms_api_helpers"] @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_locations_success(self, mock_get, mock_librenms_config): """Verify retrieving all locations.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "locations": [{"id": 1, "location": "DC1"}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, locations = api.get_locations() assert success is True assert len(locations) == 1 @patch("netbox_librenms_plugin.librenms_api.requests.post") def test_add_location_success(self, mock_post, mock_librenms_config): """Verify successful location addition.""" mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = { "status": "ok", "message": "Location created #5", } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, result_dict = api.add_location(location_data={"location": "DC2"}) assert success is True assert result_dict["id"] == "5" assert "message" in result_dict @patch("netbox_librenms_plugin.librenms_api.requests.post") def test_add_location_error(self, mock_post, mock_librenms_config): """Verify location addition error handling.""" mock_post.return_value.status_code = 500 mock_post.return_value.json.return_value = { "status": "error", "message": "Invalid location data", } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, error_msg = api.add_location(location_data={}) assert success is False assert "Invalid location data" in error_msg @patch("netbox_librenms_plugin.librenms_api.requests.patch") def test_update_location_success(self, mock_patch, mock_librenms_config): """Verify successful location update.""" mock_patch.return_value.status_code = 200 mock_patch.return_value.json.return_value = { "status": "ok", "message": "Location updated", } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, message = api.update_location(location_name="DC1", location_data={"location": "DC1-Updated"}) assert success is True @patch("netbox_librenms_plugin.librenms_api.requests.patch") def test_update_location_not_found(self, mock_patch, mock_librenms_config): """Verify updating non-existent location.""" mock_patch.return_value.status_code = 404 mock_patch.return_value.json.return_value = { "status": "error", "message": "Location not found", } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, message = api.update_location(location_name="NonExistent", location_data={}) assert success is False # ============================================================================= # Test Class 7: Ports and Inventory (9 tests) # ============================================================================= class TestLibreNMSAPIPortsAndInventory: """Test ports and inventory operations.""" pytest_plugins = ["netbox_librenms_plugin.tests.test_librenms_api_helpers"] @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_ports_all(self, mock_get, mock_librenms_config): """Verify retrieving all ports for a device.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "ports": [{"port_id": 1}, {"port_id": 2}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, data = api.get_ports(device_id=123) assert success is True assert "ports" in data assert len(data["ports"]) == 2 @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_port_by_id_success(self, mock_get, mock_librenms_config): """Verify retrieving port by ID.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "port": [{"port_id": 1, "ifName": "GigabitEthernet0/1"}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, port_data = api.get_port_by_id(port_id=1) assert success is True assert port_data is not None @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_port_by_id_error(self, mock_get, mock_librenms_config): """Verify handling of port retrieval error.""" mock_get.side_effect = requests.exceptions.RequestException("Connection error") from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, error_msg = api.get_port_by_id(port_id=999) assert success is False assert isinstance(error_msg, str) @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_inventory_success(self, mock_get, mock_librenms_config): """Verify retrieving device inventory.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "inventory": [{"entPhysicalClass": "chassis"}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, inventory = api.get_device_inventory(device_id=123) assert success is True assert len(inventory) == 1 @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_inventory_filtered_by_class(self, mock_get, mock_librenms_config): """Verify filtering inventory by physical class.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "inventory": [{"entPhysicalClass": "chassis", "entPhysicalName": "Chassis"}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, inventory = api.get_inventory_filtered(device_id=123, ent_physical_class="chassis") assert success is True assert len(inventory) == 1 @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_inventory_filtered_by_container(self, mock_get, mock_librenms_config): """Verify filtering inventory by container.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "inventory": [{"entPhysicalContainedIn": "0"}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, inventory = api.get_inventory_filtered(device_id=123, ent_physical_contained_in=0) assert success is True @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_links_success(self, mock_get, mock_librenms_config): """Verify retrieving device links.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "links": [{"id": 1, "local_port_id": 10, "remote_port_id": 20}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, links_dict = api.get_device_links(device_id=123) assert success is True assert "links" in links_dict assert len(links_dict["links"]) == 1 @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_ips_success(self, mock_get, mock_librenms_config): """Verify retrieving device IP addresses.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "addresses": [{"ipv4_address": "10.0.0.1"}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, ips = api.get_device_ips(device_id=123) assert success is True assert len(ips) == 1 @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_device_ips_empty(self, mock_get, mock_librenms_config): """Verify handling device with no IPs.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok", "addresses": []} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, ips = api.get_device_ips(device_id=123) assert success is True assert len(ips) == 0 # ============================================================================= # Test Class 8: Poller and Devices (4 tests) # ============================================================================= class TestLibreNMSAPIPollerAndDevices: """Test poller groups and device listing operations.""" pytest_plugins = ["netbox_librenms_plugin.tests.test_librenms_api_helpers"] @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_poller_groups_success(self, mock_get, mock_librenms_config): """Verify retrieving poller groups.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "get_poller_group": [{"id": 1, "group_name": "primary"}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, groups = api.get_poller_groups() assert success is True assert len(groups) == 1 @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_get_poller_groups_empty(self, mock_get, mock_librenms_config): """Verify handling empty poller groups.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "get_poller_group": [], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, groups = api.get_poller_groups() assert success is True assert len(groups) == 0 @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_list_devices_all(self, mock_get, mock_librenms_config): """Verify listing all devices.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "status": "ok", "devices": [{"device_id": 1}, {"device_id": 2}, {"device_id": 3}], } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, devices = api.list_devices() assert success is True assert len(devices) == 3 @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_list_devices_empty(self, mock_get, mock_librenms_config): """Verify handling empty device list.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"status": "ok", "devices": []} from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, devices = api.list_devices() assert success is True assert len(devices) == 0 # ============================================================================= # Test Class 9: Error Handling (6 tests) # ============================================================================= class TestLibreNMSAPIErrorHandling: """Test error handling and edge cases.""" pytest_plugins = ["netbox_librenms_plugin.tests.test_librenms_api_helpers"] @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_network_error_handling(self, mock_get, mock_librenms_config): """Verify handling of network errors.""" mock_get.side_effect = requests.exceptions.ConnectionError("Network unreachable") from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, result = api.get_device_info(device_id=123) assert success is False assert result is None @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_timeout_error_handling(self, mock_get, mock_librenms_config): """Verify handling of timeout errors.""" mock_get.side_effect = requests.exceptions.Timeout("Request timed out") from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, result = api.get_device_info(device_id=123) assert success is False assert result is None @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_invalid_json_response(self, mock_get, mock_librenms_config): """Verify handling of invalid JSON responses — ValueError is now caught gracefully.""" mock_get.return_value.status_code = 200 mock_get.return_value.json.side_effect = ValueError("Invalid JSON") from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") # ValueError is now caught, returning (False, None) instead of propagating success, result = api.get_device_info(device_id=123) assert success is False assert result is None @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_http_500_error_handling(self, mock_get, mock_librenms_config): """Verify handling of HTTP 500 errors.""" mock_get.return_value.status_code = 500 mock_get.return_value.json.return_value = { "status": "error", "message": "Internal server error", } from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") success, result = api.get_device_info(device_id=123) assert success is False assert result is None @patch("netbox_librenms_plugin.librenms_api.requests.post") def test_malformed_api_response(self, mock_post, mock_librenms_config): """Verify handling of malformed API responses.""" mock_post.return_value.status_code = 200 mock_post.return_value.json.return_value = {} # Missing expected fields from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") result = api.add_device( data={ "hostname": "test.example.com", "snmp_version": "v2c", "community": "public", } ) # Should handle missing fields gracefully assert result[0] is False @patch("netbox_librenms_plugin.librenms_api.requests.get") def test_ssl_verification_error(self, mock_get, mock_librenms_config): """Verify handling of SSL verification errors.""" mock_get.side_effect = requests.exceptions.SSLError("SSL certificate verification failed") from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") result = api.test_connection() assert result.get("error") is True # ==================================================================================== # Test Class 5-9 continuing in next part due to length... # Run `make unittest` to execute all tests # ==================================================================================== # ==================================================================================== # Guard tests: int conversion guard and VLAN dict guard # ==================================================================================== class TestGetLibreNMSIdIntGuard: """Tests for the int conversion guard in get_librenms_id.""" def test_non_integer_string_from_ip_returns_none(self, mock_librenms_config): """When get_device_id_by_ip returns a non-int string, _store_librenms_id must not be called.""" from unittest.mock import MagicMock, patch from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") obj = MagicMock() obj.primary_ip.address.ip = "192.168.1.1" obj.primary_ip.dns_name = "" obj.name = "test-device" obj._meta.model_name = "device" obj.pk = 1 with ( patch.object(api, "_get_cache_key", return_value="test_cache_key"), patch("netbox_librenms_plugin.librenms_api.cache") as mock_cache, patch.object(api, "get_device_id_by_ip", return_value="not-an-int"), patch.object(api, "get_device_id_by_hostname", return_value=None), patch.object(api, "_store_librenms_id") as mock_store, ): mock_cache.get.return_value = None result = api.get_librenms_id(obj) assert result is None mock_store.assert_not_called() def test_valid_integer_string_stores_and_returns(self, mock_librenms_config): """When get_device_id_by_ip returns a valid int string, it should be stored and returned.""" from unittest.mock import MagicMock, patch from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") obj = MagicMock() obj.primary_ip.address.ip = "192.168.1.1" obj.primary_ip.dns_name = "" obj.name = "test-device" obj._meta.model_name = "device" obj.pk = 1 with ( patch.object(api, "_get_cache_key", return_value="test_cache_key"), patch("netbox_librenms_plugin.librenms_api.cache") as mock_cache, patch.object(api, "get_device_id_by_ip", return_value="42"), patch.object(api, "_store_librenms_id") as mock_store, ): mock_cache.get.return_value = None result = api.get_librenms_id(obj) assert result == 42 mock_store.assert_called_once_with(obj, 42) class TestVlanEntryDictGuard: """Tests for the isinstance(vlan_entry, dict) guard in _parse_port_vlan_info.""" def test_non_dict_entry_is_skipped(self, mock_librenms_config): """Non-dict entries in vlans_data should be skipped without error.""" from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") port_data = { "ifName": "eth0", "ifDescr": "eth0", "port_id": 1, "ifTrunk": "dot1Q", "ifVlan": None, "vlans": [{"vlan": 10, "untagged": 1}, "bad_entry", {"vlan": 20}], } result = api.parse_port_vlan_data(port_data) # Only VIDs 10 and 20 should be parsed; string entry is skipped assert result["untagged_vlan"] == 10 assert 20 in result["tagged_vlans"] assert len(result["tagged_vlans"]) == 1 def test_mixed_bad_entries_no_exception(self, mock_librenms_config): """Multiple non-dict entries mixed with valid dicts should not raise.""" from netbox_librenms_plugin.librenms_api import LibreNMSAPI api = LibreNMSAPI(server_key="default") port_data = { "ifName": "eth0", "ifDescr": "eth0", "port_id": 2, "ifTrunk": "dot1Q", "ifVlan": None, "vlans": [None, 123, "unexpected", {"vlan": 30}], } result = api.parse_port_vlan_data(port_data) assert result["tagged_vlans"] == [30] assert result["untagged_vlan"] is None