mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-15 22:51:37 -07:00
Merge pull request #1615 from netalertx/next_release
✅ Code checks / check-url-paths (push) Has been cancelled
✅ Code checks / lint (push) Has been cancelled
✅ Code checks / docker-tests (push) Has been cancelled
🐳 👩💻 docker dev / docker_dev (push) Has been cancelled
📘 Deploy MkDocs / deploy (push) Has been cancelled
✅ Code checks / check-url-paths (push) Has been cancelled
✅ Code checks / lint (push) Has been cancelled
✅ Code checks / docker-tests (push) Has been cancelled
🐳 👩💻 docker dev / docker_dev (push) Has been cancelled
📘 Deploy MkDocs / deploy (push) Has been cancelled
Next release
This commit is contained in:
@@ -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
|
||||
|
||||
+11
-5
@@ -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
|
||||
|
||||
@@ -10,9 +10,18 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/language/lang.php';
|
||||
function renderFilterDropdown($headerKey, $columnName, $values) {
|
||||
// Generate dropdown options
|
||||
$optionsHtml = '<option value="" selected>All</option>'; // Default "All" option
|
||||
foreach ($values as $value) {
|
||||
$escapedValue = htmlspecialchars($value);
|
||||
$optionsHtml .= '<option value="' . $escapedValue . '">' . $escapedValue . '</option>';
|
||||
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 .= '<option value="' . $escapedValue . '">' . $escapedLabel . '</option>';
|
||||
}
|
||||
|
||||
// Generate the dropdown HTML
|
||||
|
||||
+29
-25
@@ -68,42 +68,46 @@ sql_devices_all = """
|
||||
"""
|
||||
|
||||
sql_appevents = """select * from AppEvents order by dateTimeCreated desc"""
|
||||
sql_devices_filters = """
|
||||
SELECT DISTINCT 'devSite' AS columnName, devSite AS columnValue
|
||||
FROM Devices WHERE devSite NOT IN ('', 'null') AND devSite IS NOT NULL
|
||||
sql_devices_filters = f"""
|
||||
SELECT DISTINCT 'devSite' AS columnName, devSite AS columnValue, devSite AS columnLabel
|
||||
FROM Devices WHERE devSite NOT IN ({NULL_EQUIVALENTS_SQL}) AND devSite IS NOT NULL
|
||||
UNION
|
||||
SELECT DISTINCT 'devSourcePlugin' AS columnName, devSourcePlugin AS columnValue
|
||||
FROM Devices WHERE devSourcePlugin NOT IN ('', 'null') AND devSourcePlugin IS NOT NULL
|
||||
SELECT DISTINCT 'devSourcePlugin' AS columnName, devSourcePlugin AS columnValue, devSourcePlugin AS columnLabel
|
||||
FROM Devices WHERE devSourcePlugin NOT IN ({NULL_EQUIVALENTS_SQL}) AND devSourcePlugin IS NOT NULL
|
||||
UNION
|
||||
SELECT DISTINCT 'devOwner' AS columnName, devOwner AS columnValue
|
||||
FROM Devices WHERE devOwner NOT IN ('', 'null') AND devOwner IS NOT NULL
|
||||
SELECT DISTINCT 'devOwner' AS columnName, devOwner AS columnValue, devOwner AS columnLabel
|
||||
FROM Devices WHERE devOwner NOT IN ({NULL_EQUIVALENTS_SQL}) AND devOwner IS NOT NULL
|
||||
UNION
|
||||
SELECT DISTINCT 'devType' AS columnName, devType AS columnValue
|
||||
FROM Devices WHERE devType NOT IN ('', 'null') AND devType IS NOT NULL
|
||||
SELECT DISTINCT 'devType' AS columnName, devType AS columnValue, devType AS columnLabel
|
||||
FROM Devices WHERE devType NOT IN ({NULL_EQUIVALENTS_SQL}) AND devType IS NOT NULL
|
||||
UNION
|
||||
SELECT DISTINCT 'devGroup' AS columnName, devGroup AS columnValue
|
||||
FROM Devices WHERE devGroup NOT IN ('', 'null') AND devGroup IS NOT NULL
|
||||
SELECT DISTINCT 'devGroup' AS columnName, devGroup AS columnValue, devGroup AS columnLabel
|
||||
FROM Devices WHERE devGroup NOT IN ({NULL_EQUIVALENTS_SQL}) AND devGroup IS NOT NULL
|
||||
UNION
|
||||
SELECT DISTINCT 'devLocation' AS columnName, devLocation AS columnValue
|
||||
FROM Devices WHERE devLocation NOT IN ('', 'null') AND devLocation IS NOT NULL
|
||||
SELECT DISTINCT 'devLocation' AS columnName, devLocation AS columnValue, devLocation AS columnLabel
|
||||
FROM Devices WHERE devLocation NOT IN ({NULL_EQUIVALENTS_SQL}) AND devLocation IS NOT NULL
|
||||
UNION
|
||||
SELECT DISTINCT 'devVendor' AS columnName, devVendor AS columnValue
|
||||
FROM Devices WHERE devVendor NOT IN ('', 'null') AND devVendor IS NOT NULL
|
||||
SELECT DISTINCT 'devVendor' AS columnName, devVendor AS columnValue, devVendor AS columnLabel
|
||||
FROM Devices WHERE devVendor NOT IN ({NULL_EQUIVALENTS_SQL}) AND devVendor IS NOT NULL
|
||||
UNION
|
||||
SELECT DISTINCT 'devSyncHubNode' AS columnName, devSyncHubNode AS columnValue
|
||||
FROM Devices WHERE devSyncHubNode NOT IN ('', 'null') AND devSyncHubNode IS NOT NULL
|
||||
SELECT DISTINCT 'devSyncHubNode' AS columnName, devSyncHubNode AS columnValue, devSyncHubNode AS columnLabel
|
||||
FROM Devices WHERE devSyncHubNode NOT IN ({NULL_EQUIVALENTS_SQL}) AND devSyncHubNode IS NOT NULL
|
||||
UNION
|
||||
SELECT DISTINCT 'devVlan' AS columnName, devVlan AS columnValue
|
||||
FROM Devices WHERE devVlan NOT IN ('', 'null') AND devVlan IS NOT NULL
|
||||
SELECT DISTINCT 'devVlan' AS columnName, devVlan AS columnValue, devVlan AS columnLabel
|
||||
FROM Devices WHERE devVlan NOT IN ({NULL_EQUIVALENTS_SQL}) 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_EQUIVALENTS_SQL}) AND d.devParentMAC IS NOT NULL
|
||||
GROUP BY d.devParentMAC COLLATE NOCASE
|
||||
UNION
|
||||
SELECT DISTINCT 'devParentRelType' AS columnName, devParentRelType AS columnValue
|
||||
FROM Devices WHERE devParentRelType NOT IN ('', 'null') AND devParentRelType IS NOT NULL
|
||||
SELECT DISTINCT 'devParentRelType' AS columnName, devParentRelType AS columnValue, devParentRelType AS columnLabel
|
||||
FROM Devices WHERE devParentRelType NOT IN ({NULL_EQUIVALENTS_SQL}) AND devParentRelType IS NOT NULL
|
||||
UNION
|
||||
SELECT DISTINCT 'devSSID' AS columnName, devSSID AS columnValue
|
||||
FROM Devices WHERE devSSID NOT IN ('', 'null') AND devSSID IS NOT NULL
|
||||
SELECT DISTINCT 'devSSID' AS columnName, devSSID AS columnValue, devSSID AS columnLabel
|
||||
FROM Devices WHERE devSSID NOT IN ({NULL_EQUIVALENTS_SQL}) AND devSSID IS NOT NULL
|
||||
ORDER BY columnName;
|
||||
"""
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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 = str(get_setting_value("SYNC_node_name") or "").strip()
|
||||
|
||||
if not node_name:
|
||||
return
|
||||
|
||||
sql.execute(
|
||||
f"""
|
||||
UPDATE Devices
|
||||
SET devSyncHubNode = ?
|
||||
WHERE COALESCE(LOWER(TRIM(devSyncHubNode)), '') IN ({NULL_EQUIVALENTS_SQL})
|
||||
""",
|
||||
(node_name,),
|
||||
)
|
||||
|
||||
db.commitDB()
|
||||
|
||||
|
||||
def update_devices_data_from_scan(db):
|
||||
sql = db.sql
|
||||
|
||||
@@ -378,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):
|
||||
@@ -406,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")
|
||||
|
||||
@@ -529,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,
|
||||
@@ -544,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, '')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Tests for update_sync_hub_node backfill."""
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _make_db(devices):
|
||||
"""Create an in-memory DB with full schema and seed rows."""
|
||||
conn = make_db()
|
||||
cur = conn.cursor()
|
||||
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"] == ""
|
||||
Reference in New Issue
Block a user