""" Integration tests using the mock LibreNMS HTTP server. These tests verify that LibreNMSAPI correctly parses responses from a real (but local, mocked) HTTP server, and that the full request/response cycle works. No Django database access is used; NetBox model interactions are mocked. """ import json import pytest from netbox_librenms_plugin.tests.mock_librenms_server import librenms_mock_server @pytest.fixture def mock_server(): with librenms_mock_server() as server: yield server def _make_api(url, token="test-token"): """Create a LibreNMSAPI instance pointed at the mock server.""" from unittest.mock import patch from netbox_librenms_plugin.librenms_api import LibreNMSAPI servers_config = { "test": { "librenms_url": url, "api_token": token, "cache_timeout": 0, "verify_ssl": False, } } with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_cfg: mock_cfg.side_effect = lambda _plugin, key: servers_config if key == "servers" else None api = LibreNMSAPI(server_key="test") assert api.server_key == "test" return api class TestMockServerSanity: """The mock server itself must start, serve, and stop cleanly.""" def test_server_starts_and_responds(self, mock_server): import urllib.request mock_server.register("/api/v0/test", {"status": "ok"}) with urllib.request.urlopen(f"{mock_server.url}/api/v0/test") as resp: data = json.loads(resp.read()) assert data["status"] == "ok" def test_404_for_unregistered_path(self, mock_server): import urllib.request from urllib.error import HTTPError try: urllib.request.urlopen(f"{mock_server.url}/api/v0/nonexistent") except HTTPError as e: assert e.code == 404 else: pytest.fail("Expected 404 HTTPError") class TestLibreNMSAPIPortsFetch: """LibreNMSAPI.get_ports() correctly parses mock server responses.""" def test_get_ports_returns_dict_with_ports_key(self, mock_server): """ get_ports() returns a parsed dict and sends the required query parameters. A callable route is used so we can capture the outgoing query string and assert that both the ``columns`` field list and ``with=vlans`` are present — if either is ever dropped, the sync page will silently lose data. """ captured_query: dict = {} ports_body = { "status": "ok", "ports": [ { "port_id": 101, "ifName": "GigabitEthernet0/1", "ifDescr": "GigabitEthernet0/1", "ifType": "ethernetCsmacd", "ifSpeed": 1_000_000_000, "ifAdminStatus": "up", "ifAlias": "uplink", "ifPhysAddress": "aa:bb:cc:dd:ee:01", "ifMtu": 1500, "ifVlan": 1, "ifTrunk": 0, } ], } def _route(method, path, query, headers, body): # query is already parsed by the mock server (dict of lists) captured_query.update(query) return 200, ports_body mock_server.routes["/api/v0/devices/1/ports"] = _route api = _make_api(mock_server.url) success, data = api.get_ports(1) assert success is True assert isinstance(data, dict) assert "ports" in data assert data["ports"][0]["ifName"] == "GigabitEthernet0/1" assert "columns" in captured_query, "get_ports() must send a 'columns' query param" assert "vlans" in captured_query.get("with", []), "get_ports() must request 'with=vlans'" def test_get_ports_returns_false_on_auth_error(self, mock_server): mock_server.auth_error_response(path="/api/v0/devices/1/ports") api = _make_api(mock_server.url) success, _ = api.get_ports(1) assert success is False def test_get_ports_empty_list_when_no_ports(self, mock_server): mock_server.register("/api/v0/devices/99/ports", {"status": "ok", "ports": []}) api = _make_api(mock_server.url) success, data = api.get_ports(99) assert success is True assert data["ports"] == [] class TestLibreNMSAPIDeviceInfo: """LibreNMSAPI.get_device_info() correctly parses device details.""" def test_returns_device_info_dict(self, mock_server): mock_server.device_info_response(device_id=5, hostname="rtr01", hardware="ISR4351") api = _make_api(mock_server.url) success, info = api.get_device_info(5) assert success is True assert isinstance(info, dict) assert info["hostname"] == "rtr01" def test_returns_false_on_404(self, mock_server): # /api/v0/devices/999 not registered → 404 api = _make_api(mock_server.url) success, info = api.get_device_info(999) assert success is False assert info is None class TestLibreNMSAPIAddDevice: """LibreNMSAPI.add_device() posts correctly and interprets the response.""" def test_add_device_success(self, mock_server): mock_server.add_device_response(device_id=10) api = _make_api(mock_server.url) success, message = api.add_device( { "hostname": "switch1.example.com", "snmp_version": "v2c", "community": "public", "force_add": False, } ) assert success is True def test_add_device_failure_on_server_error(self, mock_server): mock_server.register("/api/v0/devices", {"status": "error", "message": "duplicate"}, status=500) api = _make_api(mock_server.url) success, message = api.add_device( { "hostname": "dup.example.com", "snmp_version": "v2c", "community": "public", } ) assert success is False class TestLibreNMSAPIInventory: """LibreNMSAPI.get_device_inventory() correctly parses mock server responses.""" def test_returns_inventory_list(self, mock_server): inventory = [ { "entPhysicalIndex": 1, "entPhysicalDescr": "Chassis", "entPhysicalClass": "chassis", "entPhysicalSerialNum": "SN-CHASSIS-001", "entPhysicalModelName": "WS-C4900M", "entPhysicalName": "Chassis 1", "entPhysicalContainedIn": 0, }, { "entPhysicalIndex": 2, "entPhysicalDescr": "Linecard", "entPhysicalClass": "module", "entPhysicalSerialNum": "SN-CARD-002", "entPhysicalModelName": "WS-X4748-RJ45V+E", "entPhysicalName": "Slot 1", "entPhysicalContainedIn": 1, }, ] mock_server.register("/api/v0/inventory/7/all", {"status": "ok", "inventory": inventory}) api = _make_api(mock_server.url) success, data = api.get_device_inventory(7) assert success is True assert isinstance(data, list) assert len(data) == 2 assert data[0]["entPhysicalClass"] == "chassis" assert data[1]["entPhysicalModelName"] == "WS-X4748-RJ45V+E" def test_returns_empty_list_when_no_inventory(self, mock_server): mock_server.register("/api/v0/inventory/99/all", {"status": "ok", "inventory": []}) api = _make_api(mock_server.url) success, data = api.get_device_inventory(99) assert success is True assert data == [] def test_returns_false_on_network_error(self, mock_server): # Unregistered path → 404 → raise_for_status → RequestException api = _make_api(mock_server.url) success, _ = api.get_device_inventory(404) assert success is False def test_inventory_items_preserve_all_fields(self, mock_server): inventory = [ { "entPhysicalIndex": 5, "entPhysicalDescr": "10 Gigabit Ethernet Module", "entPhysicalClass": "module", "entPhysicalSerialNum": "JAE123XYZ", "entPhysicalModelName": "X2-10GB-LR", "entPhysicalName": "TenGigabitEthernet1/1", "entPhysicalContainedIn": 1, "entPhysicalParentRelPos": 1, } ] mock_server.register("/api/v0/inventory/3/all", {"status": "ok", "inventory": inventory}) api = _make_api(mock_server.url) success, data = api.get_device_inventory(3) assert success is True item = data[0] assert item["entPhysicalParentRelPos"] == 1 assert item["entPhysicalSerialNum"] == "JAE123XYZ" class TestLibreNMSAPIDiscovery: """ LibreNMSAPI device-ID discovery: lookup by IP and hostname fallback. Covers get_device_id_by_ip(), get_device_id_by_hostname(), and the get_librenms_id() fallback chain (IP → DNS name → hostname). """ _DEVICE_RESPONSE = { "status": "ok", "devices": [{"device_id": 42, "hostname": "sw01.example.com"}], } def test_get_device_id_by_ip_returns_id(self, mock_server): mock_server.register("/api/v0/devices/10.0.0.1", self._DEVICE_RESPONSE) api = _make_api(mock_server.url) device_id = api.get_device_id_by_ip("10.0.0.1") assert device_id == 42 def test_get_device_id_by_ip_returns_none_on_404(self, mock_server): # no route registered → 404 api = _make_api(mock_server.url) device_id = api.get_device_id_by_ip("10.0.0.2") assert device_id is None def test_get_device_id_by_hostname_returns_id(self, mock_server): mock_server.register("/api/v0/devices/sw01.example.com", self._DEVICE_RESPONSE) api = _make_api(mock_server.url) device_id = api.get_device_id_by_hostname("sw01.example.com") assert device_id == 42 def test_get_device_id_by_hostname_returns_none_on_404(self, mock_server): api = _make_api(mock_server.url) device_id = api.get_device_id_by_hostname("unknown.example.com") assert device_id is None def test_get_librenms_id_resolves_by_ip(self, mock_server): """get_librenms_id() resolves via IP when the device has a primary_ip.""" from unittest.mock import MagicMock, patch mock_server.register("/api/v0/devices/10.0.0.10", self._DEVICE_RESPONSE) api = _make_api(mock_server.url) obj = MagicMock() obj.cf = {} # no stored ID obj.primary_ip.address.ip = "10.0.0.10" obj.primary_ip.dns_name = None obj.name = "sw01" with patch.object(api, "_get_cache_key", return_value="test-key"): with patch("netbox_librenms_plugin.librenms_api.cache") as mock_cache: mock_cache.get.return_value = None with patch.object(api, "_store_librenms_id"): result = api.get_librenms_id(obj) assert result == 42 def test_get_librenms_id_falls_back_to_hostname_when_ip_fails(self, mock_server): """get_librenms_id() falls back to hostname when IP lookup returns no result.""" from unittest.mock import MagicMock, patch # IP path returns 404 (unregistered) → fallback to hostname mock_server.register("/api/v0/devices/sw01.example.com", self._DEVICE_RESPONSE) api = _make_api(mock_server.url) obj = MagicMock() obj.cf = {} # no stored ID obj.primary_ip.address.ip = "192.0.2.1" # unregistered → 404 → None obj.primary_ip.dns_name = None obj.name = "sw01.example.com" with patch.object(api, "_get_cache_key", return_value="test-key"): with patch("netbox_librenms_plugin.librenms_api.cache") as mock_cache: mock_cache.get.return_value = None with patch.object(api, "_store_librenms_id"): result = api.get_librenms_id(obj) assert result == 42 class TestLibreNMSAPIErrorResponses: """Integration tests: API client handles unusual server responses gracefully.""" def test_401_returns_false(self, mock_server): """HTTP 401 Unauthorized must return (False, ...) not raise.""" api = _make_api(mock_server.url) mock_server.register("/api/v0/devices", {"message": "Unauthorized"}, status=401) success, data = api.list_devices() assert success is False def test_500_returns_false(self, mock_server): """HTTP 500 must return (False, ...) not raise.""" api = _make_api(mock_server.url) mock_server.register("/api/v0/devices", {"message": "Internal error"}, status=500) success, data = api.list_devices() assert success is False def test_null_inventory_returns_false(self, mock_server): """{"status":"ok","inventory":null} must not raise, must return (False, error_string).""" api = _make_api(mock_server.url) mock_server.register("/api/v0/inventory/1/all", {"status": "ok", "inventory": None}) success, data = api.get_device_inventory(1) assert success is False assert isinstance(data, str) and data, "expected a non-empty error string describing the malformed inventory" def test_null_devices_field_get_device_info(self, mock_server): """{"devices": null} in get_device_info must return (False, None) not crash.""" api = _make_api(mock_server.url) mock_server.register("/api/v0/devices/42", {"devices": None}) success, data = api.get_device_info(42) assert success is False assert data is None def test_empty_devices_list_get_device_info(self, mock_server): """{"devices": []} in get_device_info must return (False, None).""" api = _make_api(mock_server.url) mock_server.register("/api/v0/devices/42", {"devices": []}) success, data = api.get_device_info(42) assert success is False assert data is None def test_404_get_device_info(self, mock_server): """404 on device endpoint must return (False, None).""" api = _make_api(mock_server.url) mock_server.register("/api/v0/devices/99", {"message": "Device not found"}, status=404) success, data = api.get_device_info(99) assert success is False assert data is None