diff --git a/server/plugins/dockerdisc/config.json b/server/plugins/dockerdisc/config.json index 3b654589..b3182998 100644 --- a/server/plugins/dockerdisc/config.json +++ b/server/plugins/dockerdisc/config.json @@ -544,13 +544,13 @@ "name": [ { "language_code": "en_us", - "string": "Docker Host MAC Address (Fallback)" + "string": "Docker Host MAC Address (Manual)" } ], "description": [ { "language_code": "en_us", - "string": "Manual fallback physical MAC address of the Docker host, used if auto-detecting it via the Socket Proxy /info endpoint fails. The host must already exist as a device in NetAlertX (found via ARP/Nmap) - this plugin never creates it." + "string": "Manually configured physical MAC address of the Docker host. When set, it is used immediately - no Socket Proxy /info call is made. Leave blank to auto-detect the host via hostname matching instead. The host must already exist as a device in NetAlertX (found via ARP/Nmap) - this plugin never creates it." } ] } @@ -620,7 +620,7 @@ "description": [ { "language_code": "en_us", - "string": "One entry per Docker host to track. Each entry pairs a read-only Docker Socket Proxy URL with that host's device (auto-detected, or entered manually as a fallback). Every container found on a host is listed under that host's own Device Details -> Plugins -> DOCKERDISC tab - the host device must already exist in NetAlertX (via ARP/Nmap); this plugin never creates devices." + "string": "One entry per Docker host to track. Each entry pairs a read-only Docker Socket Proxy URL with that host's device, matched either by a manually configured MAC (used immediately when set) or by auto-detected hostname. Every container found on a host is listed under that host's own Device Details -> Plugins -> DOCKERDISC tab - the host device must already exist in NetAlertX (via ARP/Nmap); this plugin never creates devices." } ] }, diff --git a/test/backend/test_device_instance.py b/test/backend/test_device_instance.py index 8ad4cf88..6ddd2c1c 100644 --- a/test/backend/test_device_instance.py +++ b/test/backend/test_device_instance.py @@ -60,5 +60,37 @@ class TestGetAllByName(unittest.TestCase): self.assertEqual(results, []) +class TestGetByMac(unittest.TestCase): + """devMac is declared COLLATE NOCASE at the column level (unlike + devName), so getByMac() relies on the schema rather than applying its + own COLLATE clause - this exercises that guarantee against a real + SQLite connection, not a mock.""" + + def setUp(self): + self.conn = make_db() + insert_device_from_dict(self.conn, make_device_dict("aa:bb:cc:dd:ee:ff")) + self.conn.commit() + + def _instance(self): + from models.device_instance import DeviceInstance + inst = DeviceInstance() + + def _fetchone(q, p=()): + row = self.conn.execute(q, p).fetchone() + return dict(row) if row else None + inst._fetchone = _fetchone + return inst + + def test_case_insensitive_match(self): + inst = self._instance() + result = inst.getByMac("AA:BB:CC:DD:EE:FF") + self.assertIsNotNone(result) + self.assertEqual(result["devMac"], "aa:bb:cc:dd:ee:ff") + + def test_no_match_returns_none(self): + inst = self._instance() + self.assertIsNone(inst.getByMac("00:00:00:00:00:00")) + + if __name__ == "__main__": unittest.main() diff --git a/test/db_test_helpers.py b/test/db_test_helpers.py index 646f8526..66763ac2 100644 --- a/test/db_test_helpers.py +++ b/test/db_test_helpers.py @@ -28,7 +28,7 @@ from db.db_history import ensure_deviceshistory_table, ensure_deviceshistory_tri CREATE_DEVICES = """ CREATE TABLE IF NOT EXISTS Devices ( - devMac TEXT PRIMARY KEY, + devMac TEXT PRIMARY KEY COLLATE NOCASE, devName TEXT, devOwner TEXT, devType TEXT, diff --git a/test/plugins/test_dockerdisc.py b/test/plugins/test_dockerdisc.py index 7f425091..582e834a 100644 --- a/test/plugins/test_dockerdisc.py +++ b/test/plugins/test_dockerdisc.py @@ -445,20 +445,18 @@ def test_lookup_device_mac_not_found(): assert dockerdisc.lookup_device_mac("aa:bb:cc:dd:ee:ff") is False -def test_lookup_device_mac_found_regardless_of_stored_mac_case(): - """The plugin always passes a lowercase, normalize_mac()'d value in - - this only guards that lookup_device_mac() doesn't do anything of its - own (e.g. an exact-string comparison) that would undo whatever - case-insensitivity DeviceInstance.getByMac() provides. The real - guarantee is schema-level - Devices.devMac is declared - `COLLATE NOCASE` (server/db/schema/app.sql), which is exactly why - getByMac() itself doesn't need to apply it explicitly (unlike - getAllByName(), which does - devName has no such column collation; - see that method's docstring and test/backend/test_device_instance.py). - This test's stub can't exercise real SQLite collation, only that this - function's own logic is agnostic to it.""" - with patch.object(dockerdisc, "DeviceInstance", _stub_device_instance(get_by_mac={"devMac": "AA:BB:CC:DD:EE:FF"})): - assert dockerdisc.lookup_device_mac("aa:bb:cc:dd:ee:ff") is True +def test_lookup_device_mac_passes_mac_through_unchanged(): + """lookup_device_mac() must not do any of its own case massaging - it + delegates entirely to DeviceInstance.getByMac(), which relies on + Devices.devMac's schema-level `COLLATE NOCASE` (server/db/schema/app.sql) + for case-insensitive matching. That guarantee is exercised against a + real SQLite connection in test/backend/test_device_instance.py's + TestGetByMac; a mocked DeviceInstance can't exercise real collation, so + this test only checks that the mac argument reaches getByMac() as-is.""" + stub = _stub_device_instance(get_by_mac={"devMac": "aa:bb:cc:dd:ee:ff"}) + with patch.object(dockerdisc, "DeviceInstance", stub): + dockerdisc.lookup_device_mac("AA:BB:CC:DD:EE:FF") + stub.return_value.getByMac.assert_called_once_with("AA:BB:CC:DD:EE:FF") # ---------------------------------------------------------------------------