From 285bd3ec22971ec84f4b74226c473fd0652acd05 Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:38:37 +0000 Subject: [PATCH 1/5] Enhance device filters: support label for dropdown options and improve SQL queries #1611 --- front/devices.php | 16 +++++++++---- front/php/components/devices_filters.php | 15 +++++++++--- server/const.py | 30 ++++++++++++++---------- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/front/devices.php b/front/devices.php index 19095e52..e08fdbc4 100755 --- a/front/devices.php +++ b/front/devices.php @@ -436,22 +436,28 @@ function initFilters() { if (existingFilter) { // Add the unique columnValue to options if not already present - if (!existingFilter.options.includes(entry.columnValue)) { - existingFilter.options.push(entry.columnValue); + if (!existingFilter.options.some(opt => opt.value === entry.columnValue)) { + existingFilter.options.push({ + value: entry.columnValue, + label: entry.columnLabel || entry.columnValue + }); } } else { // Create a new filter entry transformed.filters.push({ column: entry.columnName, headerKey: entry.columnHeaderStringKey, - options: [entry.columnValue] + options: [{ + value: entry.columnValue, + label: entry.columnLabel || entry.columnValue + }] }); } }); - // Sort options alphabetically for better readability + // Sort options alphabetically by label for better readability transformed.filters.forEach(filter => { - filter.options.sort(); + filter.options.sort((a, b) => a.label.localeCompare(b.label)); }); // Output the result diff --git a/front/php/components/devices_filters.php b/front/php/components/devices_filters.php index 0a376b4f..9f36d792 100755 --- a/front/php/components/devices_filters.php +++ b/front/php/components/devices_filters.php @@ -10,9 +10,18 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/language/lang.php'; function renderFilterDropdown($headerKey, $columnName, $values) { // Generate dropdown options $optionsHtml = ''; // Default "All" option - foreach ($values as $value) { - $escapedValue = htmlspecialchars($value); - $optionsHtml .= ''; + foreach ($values as $item) { + // Support both {value, label} objects and plain strings (backward compat) + if (is_array($item)) { + $val = $item['value'] ?? ''; + $label = $item['label'] ?? $val; + } else { + $val = $item; + $label = $item; + } + $escapedValue = htmlspecialchars($val); + $escapedLabel = htmlspecialchars($label); + $optionsHtml .= ''; } // Generate the dropdown HTML diff --git a/server/const.py b/server/const.py index 1e554c9b..34cd033a 100755 --- a/server/const.py +++ b/server/const.py @@ -69,40 +69,44 @@ sql_devices_all = """ sql_appevents = """select * from AppEvents order by dateTimeCreated desc""" sql_devices_filters = """ - SELECT DISTINCT 'devSite' AS columnName, devSite AS columnValue + SELECT DISTINCT 'devSite' AS columnName, devSite AS columnValue, devSite AS columnLabel FROM Devices WHERE devSite NOT IN ('', 'null') AND devSite IS NOT NULL UNION - SELECT DISTINCT 'devSourcePlugin' AS columnName, devSourcePlugin AS columnValue + SELECT DISTINCT 'devSourcePlugin' AS columnName, devSourcePlugin AS columnValue, devSourcePlugin AS columnLabel FROM Devices WHERE devSourcePlugin NOT IN ('', 'null') AND devSourcePlugin IS NOT NULL UNION - SELECT DISTINCT 'devOwner' AS columnName, devOwner AS columnValue + SELECT DISTINCT 'devOwner' AS columnName, devOwner AS columnValue, devOwner AS columnLabel FROM Devices WHERE devOwner NOT IN ('', 'null') AND devOwner IS NOT NULL UNION - SELECT DISTINCT 'devType' AS columnName, devType AS columnValue + SELECT DISTINCT 'devType' AS columnName, devType AS columnValue, devType AS columnLabel FROM Devices WHERE devType NOT IN ('', 'null') AND devType IS NOT NULL UNION - SELECT DISTINCT 'devGroup' AS columnName, devGroup AS columnValue + SELECT DISTINCT 'devGroup' AS columnName, devGroup AS columnValue, devGroup AS columnLabel FROM Devices WHERE devGroup NOT IN ('', 'null') AND devGroup IS NOT NULL UNION - SELECT DISTINCT 'devLocation' AS columnName, devLocation AS columnValue + SELECT DISTINCT 'devLocation' AS columnName, devLocation AS columnValue, devLocation AS columnLabel FROM Devices WHERE devLocation NOT IN ('', 'null') AND devLocation IS NOT NULL UNION - SELECT DISTINCT 'devVendor' AS columnName, devVendor AS columnValue + SELECT DISTINCT 'devVendor' AS columnName, devVendor AS columnValue, devVendor AS columnLabel FROM Devices WHERE devVendor NOT IN ('', 'null') AND devVendor IS NOT NULL UNION - SELECT DISTINCT 'devSyncHubNode' AS columnName, devSyncHubNode AS columnValue + SELECT DISTINCT 'devSyncHubNode' AS columnName, devSyncHubNode AS columnValue, devSyncHubNode AS columnLabel FROM Devices WHERE devSyncHubNode NOT IN ('', 'null') AND devSyncHubNode IS NOT NULL UNION - SELECT DISTINCT 'devVlan' AS columnName, devVlan AS columnValue + SELECT DISTINCT 'devVlan' AS columnName, devVlan AS columnValue, devVlan AS columnLabel FROM Devices WHERE devVlan NOT IN ('', 'null') AND devVlan IS NOT NULL UNION - SELECT DISTINCT 'devParentMAC' AS columnName, devParentMAC AS columnValue - FROM Devices WHERE devParentMAC NOT IN ('', 'null') AND devParentMAC IS NOT NULL + SELECT 'devParentMAC' AS columnName, d.devParentMAC AS columnValue, + COALESCE(p.devName, d.devParentMAC) AS columnLabel + FROM Devices d + LEFT JOIN Devices p ON LOWER(p.devMac) = LOWER(d.devParentMAC) + WHERE d.devParentMAC NOT IN ('', 'null') AND d.devParentMAC IS NOT NULL + GROUP BY d.devParentMAC COLLATE NOCASE UNION - SELECT DISTINCT 'devParentRelType' AS columnName, devParentRelType AS columnValue + SELECT DISTINCT 'devParentRelType' AS columnName, devParentRelType AS columnValue, devParentRelType AS columnLabel FROM Devices WHERE devParentRelType NOT IN ('', 'null') AND devParentRelType IS NOT NULL UNION - SELECT DISTINCT 'devSSID' AS columnName, devSSID AS columnValue + SELECT DISTINCT 'devSSID' AS columnName, devSSID AS columnValue, devSSID AS columnLabel FROM Devices WHERE devSSID NOT IN ('', 'null') AND devSSID IS NOT NULL ORDER BY columnName; """ From 89139fed6063f5658634733cab617999b958881f Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:59:36 +0000 Subject: [PATCH 2/5] Add update_sync_hub_node function and corresponding tests for backfilling devSyncHubNode #1609 --- server/scan/device_handling.py | 23 +++++ server/scan/session_events.py | 5 ++ test/scan/test_sync_hub_node_backfill.py | 108 +++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 test/scan/test_sync_hub_node_backfill.py diff --git a/server/scan/device_handling.py b/server/scan/device_handling.py index 63673eda..5479f471 100755 --- a/server/scan/device_handling.py +++ b/server/scan/device_handling.py @@ -240,6 +240,29 @@ def update_devLastConnection_from_CurrentScan(db): """) +def update_sync_hub_node(db): + """ + Backfill devSyncHubNode with SYNC_node_name for devices where it is empty. + Mirrors the fallback already used in create_new_devices. + """ + sql = db.sql + node_name = get_setting_value("SYNC_node_name") + + if not node_name: + return + + sql.execute( + """ + UPDATE Devices + SET devSyncHubNode = ? + WHERE COALESCE(devSyncHubNode, '') IN ('', 'null') + """, + (node_name,), + ) + + db.commitDB() + + def update_devices_data_from_scan(db): sql = db.sql diff --git a/server/scan/session_events.py b/server/scan/session_events.py index f39e791f..ca35c1fe 100755 --- a/server/scan/session_events.py +++ b/server/scan/session_events.py @@ -4,6 +4,7 @@ from scan.device_handling import ( save_scanned_devices, exclude_ignored_devices, update_devices_data_from_scan, + update_sync_hub_node, update_vendors_from_mac, update_icons_and_types, update_devPresentLastScan_based_on_force_status, @@ -62,6 +63,10 @@ def process_scan(db): mylog("verbose", "[Process Scan] Updating Devices Info") update_devices_data_from_scan(db) + # Backfill devSyncHubNode for devices where it is empty + mylog("verbose", "[Process Scan] Updating Sync Hub Node") + update_sync_hub_node(db) + # Last Connection Time stamp from CurrentScan mylog("verbose", "[Process Scan] Updating devLastConnection from CurrentScan") update_devLastConnection_from_CurrentScan(db) diff --git a/test/scan/test_sync_hub_node_backfill.py b/test/scan/test_sync_hub_node_backfill.py new file mode 100644 index 00000000..74587737 --- /dev/null +++ b/test/scan/test_sync_hub_node_backfill.py @@ -0,0 +1,108 @@ +"""Tests for update_sync_hub_node backfill.""" + +import sqlite3 +from unittest.mock import patch + +from server.scan import device_handling + + +class DummyDB: + """Minimal DB wrapper compatible with device_handling helpers.""" + + def __init__(self, conn): + self.sql = conn.cursor() + self._conn = conn + + def commitDB(self): + self._conn.commit() + + +def _make_db(devices): + """Create an in-memory DB with a Devices table and seed rows.""" + conn = sqlite3.connect(":memory:") + conn.row_factory = sqlite3.Row + cur = conn.cursor() + + cur.execute( + """ + CREATE TABLE Devices ( + devMac TEXT PRIMARY KEY, + devSyncHubNode TEXT + ) + """ + ) + + cur.executemany( + "INSERT INTO Devices (devMac, devSyncHubNode) VALUES (?, ?)", + devices, + ) + conn.commit() + return conn + + +def _read_nodes(conn): + """Return a dict of devMac -> devSyncHubNode.""" + return { + row["devMac"]: row["devSyncHubNode"] + for row in conn.execute("SELECT devMac, devSyncHubNode FROM Devices") + } + + +@patch.object(device_handling, "get_setting_value", return_value="MyNode") +def test_backfill_empty_values(mock_setting): + """Empty and null devSyncHubNode should be backfilled with SYNC_node_name.""" + conn = _make_db([ + ("AA:AA:AA:AA:AA:01", ""), + ("AA:AA:AA:AA:AA:02", None), + ("AA:AA:AA:AA:AA:03", "null"), + ]) + + device_handling.update_sync_hub_node(DummyDB(conn)) + nodes = _read_nodes(conn) + + assert nodes["AA:AA:AA:AA:AA:01"] == "MyNode" + assert nodes["AA:AA:AA:AA:AA:02"] == "MyNode" + assert nodes["AA:AA:AA:AA:AA:03"] == "MyNode" + + +@patch.object(device_handling, "get_setting_value", return_value="MyNode") +def test_no_overwrite_existing(mock_setting): + """Devices with a real devSyncHubNode should not be overwritten.""" + conn = _make_db([ + ("AA:AA:AA:AA:AA:01", "RemoteNode"), + ("AA:AA:AA:AA:AA:02", ""), + ]) + + device_handling.update_sync_hub_node(DummyDB(conn)) + nodes = _read_nodes(conn) + + assert nodes["AA:AA:AA:AA:AA:01"] == "RemoteNode" + assert nodes["AA:AA:AA:AA:AA:02"] == "MyNode" + + +@patch.object(device_handling, "get_setting_value", return_value="") +def test_noop_when_setting_empty(mock_setting): + """No updates when SYNC_node_name is empty.""" + conn = _make_db([ + ("AA:AA:AA:AA:AA:01", ""), + ("AA:AA:AA:AA:AA:02", None), + ]) + + device_handling.update_sync_hub_node(DummyDB(conn)) + nodes = _read_nodes(conn) + + assert nodes["AA:AA:AA:AA:AA:01"] == "" + assert nodes["AA:AA:AA:AA:AA:02"] is None + + +@patch.object(device_handling, "get_setting_value", return_value=None) +def test_noop_when_setting_none(mock_setting): + """No updates when SYNC_node_name is None.""" + conn = _make_db([ + ("AA:AA:AA:AA:AA:01", ""), + ]) + + device_handling.update_sync_hub_node(DummyDB(conn)) + nodes = _read_nodes(conn) + + assert nodes["AA:AA:AA:AA:AA:01"] == "" From c0462984e749b3764fed01dd72553779bee3964b Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Wed, 15 Apr 2026 10:35:09 +1000 Subject: [PATCH 3/5] BE: feedback + unify null / empty checks Signed-off-by: jokob-sk --- server/const.py | 26 +++++++++++++------------- server/scan/device_handling.py | 22 +++++++++++----------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/server/const.py b/server/const.py index 34cd033a..9762ed69 100755 --- a/server/const.py +++ b/server/const.py @@ -68,46 +68,46 @@ sql_devices_all = """ """ sql_appevents = """select * from AppEvents order by dateTimeCreated desc""" -sql_devices_filters = """ +sql_devices_filters = f""" SELECT DISTINCT 'devSite' AS columnName, devSite AS columnValue, devSite AS columnLabel - FROM Devices WHERE devSite NOT IN ('', 'null') AND devSite IS NOT NULL + FROM Devices WHERE devSite NOT IN ({NULL_EQUIVALENTS_SQL}) AND devSite IS NOT NULL UNION SELECT DISTINCT 'devSourcePlugin' AS columnName, devSourcePlugin AS columnValue, devSourcePlugin AS columnLabel - FROM Devices WHERE devSourcePlugin NOT IN ('', 'null') AND devSourcePlugin IS NOT NULL + FROM Devices WHERE devSourcePlugin NOT IN ({NULL_EQUIVALENTS_SQL}) AND devSourcePlugin IS NOT NULL UNION SELECT DISTINCT 'devOwner' AS columnName, devOwner AS columnValue, devOwner AS columnLabel - FROM Devices WHERE devOwner NOT IN ('', 'null') AND devOwner IS NOT NULL + FROM Devices WHERE devOwner NOT IN ({NULL_EQUIVALENTS_SQL}) AND devOwner IS NOT NULL UNION SELECT DISTINCT 'devType' AS columnName, devType AS columnValue, devType AS columnLabel - FROM Devices WHERE devType NOT IN ('', 'null') AND devType IS NOT NULL + FROM Devices WHERE devType NOT IN ({NULL_EQUIVALENTS_SQL}) AND devType IS NOT NULL UNION SELECT DISTINCT 'devGroup' AS columnName, devGroup AS columnValue, devGroup AS columnLabel - FROM Devices WHERE devGroup NOT IN ('', 'null') AND devGroup IS NOT NULL + FROM Devices WHERE devGroup NOT IN ({NULL_EQUIVALENTS_SQL}) AND devGroup IS NOT NULL UNION SELECT DISTINCT 'devLocation' AS columnName, devLocation AS columnValue, devLocation AS columnLabel - FROM Devices WHERE devLocation NOT IN ('', 'null') AND devLocation IS NOT NULL + FROM Devices WHERE devLocation NOT IN ({NULL_EQUIVALENTS_SQL}) AND devLocation IS NOT NULL UNION SELECT DISTINCT 'devVendor' AS columnName, devVendor AS columnValue, devVendor AS columnLabel - FROM Devices WHERE devVendor NOT IN ('', 'null') AND devVendor IS NOT NULL + FROM Devices WHERE devVendor NOT IN ({NULL_EQUIVALENTS_SQL}) AND devVendor IS NOT NULL UNION SELECT DISTINCT 'devSyncHubNode' AS columnName, devSyncHubNode AS columnValue, devSyncHubNode AS columnLabel - FROM Devices WHERE devSyncHubNode NOT IN ('', 'null') AND devSyncHubNode IS NOT NULL + FROM Devices WHERE devSyncHubNode NOT IN ({NULL_EQUIVALENTS_SQL}) AND devSyncHubNode IS NOT NULL UNION SELECT DISTINCT 'devVlan' AS columnName, devVlan AS columnValue, devVlan AS columnLabel - FROM Devices WHERE devVlan NOT IN ('', 'null') AND devVlan IS NOT NULL + FROM Devices WHERE devVlan NOT IN ({NULL_EQUIVALENTS_SQL}) AND devVlan IS NOT NULL UNION SELECT 'devParentMAC' AS columnName, d.devParentMAC AS columnValue, COALESCE(p.devName, d.devParentMAC) AS columnLabel FROM Devices d LEFT JOIN Devices p ON LOWER(p.devMac) = LOWER(d.devParentMAC) - WHERE d.devParentMAC NOT IN ('', 'null') AND d.devParentMAC IS NOT NULL + WHERE d.devParentMAC NOT IN ({NULL_EQUIVALENTS_SQL}) AND d.devParentMAC IS NOT NULL GROUP BY d.devParentMAC COLLATE NOCASE UNION SELECT DISTINCT 'devParentRelType' AS columnName, devParentRelType AS columnValue, devParentRelType AS columnLabel - FROM Devices WHERE devParentRelType NOT IN ('', 'null') AND devParentRelType IS NOT NULL + FROM Devices WHERE devParentRelType NOT IN ({NULL_EQUIVALENTS_SQL}) AND devParentRelType IS NOT NULL UNION SELECT DISTINCT 'devSSID' AS columnName, devSSID AS columnValue, devSSID AS columnLabel - FROM Devices WHERE devSSID NOT IN ('', 'null') AND devSSID IS NOT NULL + FROM Devices WHERE devSSID NOT IN ({NULL_EQUIVALENTS_SQL}) AND devSSID IS NOT NULL ORDER BY columnName; """ diff --git a/server/scan/device_handling.py b/server/scan/device_handling.py index 5479f471..d1013ec3 100755 --- a/server/scan/device_handling.py +++ b/server/scan/device_handling.py @@ -5,7 +5,7 @@ import ipaddress from helper import get_setting_value, check_IP_format from utils.datetime_utils import timeNowUTC, normalizeTimeStamp from logger import mylog, Logger -from const import vendorsPath, vendorsPathNewest, sql_generateGuid, NULL_EQUIVALENTS +from const import vendorsPath, vendorsPathNewest, sql_generateGuid, NULL_EQUIVALENTS, NULL_EQUIVALENTS_SQL from models.device_instance import DeviceInstance from scan.name_resolution import NameResolver from scan.device_heuristics import guess_icon, guess_type @@ -246,16 +246,16 @@ def update_sync_hub_node(db): Mirrors the fallback already used in create_new_devices. """ sql = db.sql - node_name = get_setting_value("SYNC_node_name") + node_name = str(get_setting_value("SYNC_node_name") or "").strip() if not node_name: return sql.execute( - """ + f""" UPDATE Devices SET devSyncHubNode = ? - WHERE COALESCE(devSyncHubNode, '') IN ('', 'null') + WHERE COALESCE(LOWER(TRIM(devSyncHubNode)), '') IN {NULL_EQUIVALENTS_SQL} """, (node_name,), ) @@ -401,11 +401,11 @@ def update_icons_and_types(db): if get_setting_value("NEWDEV_replace_preset_icon"): query = f"""SELECT * FROM Devices - WHERE devIcon in ('', 'null', '{default_icon}') + WHERE devIcon in ({NULL_EQUIVALENTS_SQL}, '{default_icon}') OR devIcon IS NULL""" else: - query = """SELECT * FROM Devices - WHERE devIcon in ('', 'null') + query = f"""SELECT * FROM Devices + WHERE devIcon in ({NULL_EQUIVALENTS_SQL}) OR devIcon IS NULL""" for device in sql.execute(query): @@ -429,8 +429,8 @@ def update_icons_and_types(db): # Guess Type recordsToUpdate = [] - query = """SELECT * FROM Devices - WHERE devType in ('', 'null') + query = f"""SELECT * FROM Devices + WHERE devType in ({NULL_EQUIVALENTS_SQL}) OR devType IS NULL""" default_type = get_setting_value("NEWDEV_devType") @@ -552,7 +552,7 @@ def save_scanned_devices(db): def print_scan_stats(db): sql = db.sql # TO-DO - query = """ + query = f""" SELECT (SELECT COUNT(*) FROM CurrentScan) AS devices_detected, (SELECT COUNT(*) FROM CurrentScan WHERE NOT EXISTS (SELECT 1 FROM Devices WHERE devMac = scanMac)) AS new_devices, @@ -567,7 +567,7 @@ def print_scan_stats(db): (SELECT COUNT(*) FROM Devices, CurrentScan WHERE devMac = scanMac AND scanLastIP IS NOT NULL - AND scanLastIP NOT IN ('', 'null', '(unknown)', '(Unknown)') + AND scanLastIP NOT IN ({NULL_EQUIVALENTS_SQL}) AND scanLastIP <> COALESCE(devPrimaryIPv4, '') AND scanLastIP <> COALESCE(devPrimaryIPv6, '') AND scanLastIP <> COALESCE(devLastIP, '') From 7c7beaaf9693ad6ad763b76cfd1548cfde2aba5d Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Wed, 15 Apr 2026 10:43:22 +1000 Subject: [PATCH 4/5] BE: empty checks fix Signed-off-by: jokob-sk --- server/scan/device_handling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/scan/device_handling.py b/server/scan/device_handling.py index d1013ec3..1029caa8 100755 --- a/server/scan/device_handling.py +++ b/server/scan/device_handling.py @@ -255,7 +255,7 @@ def update_sync_hub_node(db): f""" UPDATE Devices SET devSyncHubNode = ? - WHERE COALESCE(LOWER(TRIM(devSyncHubNode)), '') IN {NULL_EQUIVALENTS_SQL} + WHERE COALESCE(LOWER(TRIM(devSyncHubNode)), '') IN ({NULL_EQUIVALENTS_SQL}) """, (node_name,), ) From f9bd8f35f5477ada5dbc21bb7a0ed2bf9ca60232 Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Wed, 15 Apr 2026 12:48:01 +0000 Subject: [PATCH 5/5] Refactor test_sync_hub_node_backfill: streamline imports and utilize shared DB helpers --- .github/skills/code-standards/SKILL.md | 12 +++++++++ test/scan/test_sync_hub_node_backfill.py | 32 ++++++------------------ 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/.github/skills/code-standards/SKILL.md b/.github/skills/code-standards/SKILL.md index 37e6932b..faf583d6 100644 --- a/.github/skills/code-standards/SKILL.md +++ b/.github/skills/code-standards/SKILL.md @@ -74,6 +74,18 @@ Use sanitizers from `server/helper.py` before storing user input. MAC addresses - Everything is already writable - If permissions needed, fix `.devcontainer/scripts/setup.sh` +## Test Helpers — No Duplicate Mocks + +Reuse shared mocks and factories from `test/db_test_helpers.py`. Never redefine `DummyDB`, `make_db`, or inline DDL in individual test files. + +```python +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from db_test_helpers import make_db, DummyDB, insert_device, minutes_ago +``` + +If a helper you need doesn't exist yet, add it to `db_test_helpers.py` — not locally in the test file. + ## Path Hygiene - Use environment variables for runtime paths diff --git a/test/scan/test_sync_hub_node_backfill.py b/test/scan/test_sync_hub_node_backfill.py index 74587737..f32fcdac 100644 --- a/test/scan/test_sync_hub_node_backfill.py +++ b/test/scan/test_sync_hub_node_backfill.py @@ -1,37 +1,19 @@ """Tests for update_sync_hub_node backfill.""" -import sqlite3 +import sys +import os from unittest.mock import patch +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from db_test_helpers import make_db, DummyDB # noqa: E402 + from server.scan import device_handling -class DummyDB: - """Minimal DB wrapper compatible with device_handling helpers.""" - - def __init__(self, conn): - self.sql = conn.cursor() - self._conn = conn - - def commitDB(self): - self._conn.commit() - - def _make_db(devices): - """Create an in-memory DB with a Devices table and seed rows.""" - conn = sqlite3.connect(":memory:") - conn.row_factory = sqlite3.Row + """Create an in-memory DB with full schema and seed rows.""" + conn = make_db() cur = conn.cursor() - - cur.execute( - """ - CREATE TABLE Devices ( - devMac TEXT PRIMARY KEY, - devSyncHubNode TEXT - ) - """ - ) - cur.executemany( "INSERT INTO Devices (devMac, devSyncHubNode) VALUES (?, ?)", devices,