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,