mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-31 07:12:23 -07:00
feat: Enhance plugin configurations and improve MAC normalization
This commit is contained in:
@@ -115,6 +115,30 @@ class TestDeviceFieldLock:
|
||||
assert resp.status_code == 400
|
||||
assert "cannot be locked" in resp.json.get("error", "")
|
||||
|
||||
def test_lock_field_normalizes_mac(self, client, test_mac, auth_headers):
|
||||
"""Lock endpoint should normalize MACs before applying locks."""
|
||||
# Create device with normalized MAC
|
||||
self.test_create_test_device(client, test_mac, auth_headers)
|
||||
|
||||
mac_variant = "aa-bb-cc-dd-ee-ff"
|
||||
payload = {
|
||||
"fieldName": "devName",
|
||||
"lock": True
|
||||
}
|
||||
resp = client.post(
|
||||
f"/device/{mac_variant}/field/lock",
|
||||
json=payload,
|
||||
headers=auth_headers
|
||||
)
|
||||
assert resp.status_code == 200, f"Failed to lock via normalized MAC: {resp.json}"
|
||||
assert resp.json.get("locked") is True
|
||||
|
||||
# Verify source is LOCKED on normalized MAC
|
||||
resp = client.get(f"/device/{test_mac}", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
device_data = resp.json
|
||||
assert device_data.get("devNameSource") == "LOCKED"
|
||||
|
||||
def test_lock_all_tracked_fields(self, client, test_mac, auth_headers):
|
||||
"""Lock each tracked field individually."""
|
||||
# First create device
|
||||
|
||||
@@ -72,6 +72,103 @@ def ip_test_db():
|
||||
conn.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def new_device_db():
|
||||
"""Create an in-memory SQLite database for create_new_devices tests."""
|
||||
conn = sqlite3.connect(":memory:")
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE Devices (
|
||||
devMac TEXT PRIMARY KEY,
|
||||
devName TEXT,
|
||||
devVendor TEXT,
|
||||
devLastIP TEXT,
|
||||
devPrimaryIPv4 TEXT,
|
||||
devPrimaryIPv6 TEXT,
|
||||
devFirstConnection TEXT,
|
||||
devLastConnection TEXT,
|
||||
devSyncHubNode TEXT,
|
||||
devGUID TEXT,
|
||||
devParentMAC TEXT,
|
||||
devParentPort TEXT,
|
||||
devSite TEXT,
|
||||
devSSID TEXT,
|
||||
devType TEXT,
|
||||
devSourcePlugin TEXT,
|
||||
devAlertEvents INTEGER,
|
||||
devAlertDown INTEGER,
|
||||
devPresentLastScan INTEGER,
|
||||
devIsArchived INTEGER,
|
||||
devIsNew INTEGER,
|
||||
devSkipRepeated INTEGER,
|
||||
devScan INTEGER,
|
||||
devOwner TEXT,
|
||||
devFavorite INTEGER,
|
||||
devGroup TEXT,
|
||||
devComments TEXT,
|
||||
devLogEvents INTEGER,
|
||||
devLocation TEXT,
|
||||
devCustomProps TEXT,
|
||||
devParentRelType TEXT,
|
||||
devReqNicsOnline INTEGER
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE CurrentScan (
|
||||
cur_MAC TEXT,
|
||||
cur_Name TEXT,
|
||||
cur_Vendor TEXT,
|
||||
cur_ScanMethod TEXT,
|
||||
cur_IP TEXT,
|
||||
cur_SyncHubNodeName TEXT,
|
||||
cur_NetworkNodeMAC TEXT,
|
||||
cur_PORT TEXT,
|
||||
cur_NetworkSite TEXT,
|
||||
cur_SSID TEXT,
|
||||
cur_Type TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE Events (
|
||||
eve_MAC TEXT,
|
||||
eve_IP TEXT,
|
||||
eve_DateTime TEXT,
|
||||
eve_EventType TEXT,
|
||||
eve_AdditionalInfo TEXT,
|
||||
eve_PendingAlertEmail INTEGER
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE Sessions (
|
||||
ses_MAC TEXT,
|
||||
ses_IP TEXT,
|
||||
ses_EventTypeConnection TEXT,
|
||||
ses_DateTimeConnection TEXT,
|
||||
ses_EventTypeDisconnection TEXT,
|
||||
ses_DateTimeDisconnection TEXT,
|
||||
ses_StillConnected INTEGER,
|
||||
ses_AdditionalInfo TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
yield conn
|
||||
conn.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ip_handlers():
|
||||
"""Mock device_handling helper functions."""
|
||||
@@ -311,6 +408,55 @@ def test_invalid_ip_values_rejected(ip_test_db, mock_ip_handlers):
|
||||
), f"Invalid IP '{invalid_ip}' should not overwrite valid IPv4"
|
||||
|
||||
|
||||
def test_invalid_ipv6_rejected_on_create_new_devices(new_device_db):
|
||||
"""Invalid IPv6 values should not be persisted when creating new devices."""
|
||||
cur = new_device_db.cursor()
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO CurrentScan (
|
||||
cur_MAC, cur_Name, cur_Vendor, cur_ScanMethod, cur_IP,
|
||||
cur_SyncHubNodeName, cur_NetworkNodeMAC, cur_PORT,
|
||||
cur_NetworkSite, cur_SSID, cur_Type
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
"AA:BB:CC:DD:EE:10",
|
||||
"",
|
||||
"Vendor",
|
||||
"ARPSCAN",
|
||||
"fe80::zz",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
),
|
||||
)
|
||||
new_device_db.commit()
|
||||
|
||||
db = Mock()
|
||||
db.sql_connection = new_device_db
|
||||
db.sql = cur
|
||||
db.commitDB = Mock(side_effect=new_device_db.commit)
|
||||
|
||||
with patch("helper.get_setting_value", return_value=""), patch.object(
|
||||
device_handling, "get_setting_value", return_value=""
|
||||
):
|
||||
device_handling.create_new_devices(db)
|
||||
|
||||
row = cur.execute(
|
||||
"SELECT devLastIP, devPrimaryIPv4, devPrimaryIPv6 FROM Devices WHERE devMac = ?",
|
||||
("AA:BB:CC:DD:EE:10",),
|
||||
).fetchone()
|
||||
|
||||
assert row is not None, "Device should be created"
|
||||
assert row["devLastIP"] == "", "Invalid IPv6 should not set devLastIP"
|
||||
assert row["devPrimaryIPv4"] == "", "Invalid IPv6 should not set devPrimaryIPv4"
|
||||
assert row["devPrimaryIPv6"] == "", "Invalid IPv6 should not set devPrimaryIPv6"
|
||||
|
||||
|
||||
def test_ipv4_ipv6_mixed_in_multiple_scans(ip_test_db, mock_ip_handlers):
|
||||
"""Multiple scans with different IP types should set both primary fields correctly."""
|
||||
cur = ip_test_db.cursor()
|
||||
|
||||
231
test/test_device_atomicity.py
Normal file
231
test/test_device_atomicity.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Test for atomicity of device updates with source-tracking.
|
||||
|
||||
Verifies that:
|
||||
1. If source-tracking fails, the device row is rolled back.
|
||||
2. If source-tracking succeeds, device row and sources are both committed.
|
||||
3. Database remains consistent in both scenarios.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
# Add server and plugins to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'server'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'front', 'plugins'))
|
||||
|
||||
from models.device_instance import DeviceInstance # noqa: E402 [flake8 lint suppression]
|
||||
from plugin_helper import normalize_mac # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
class TestDeviceAtomicity(unittest.TestCase):
|
||||
"""Test atomic transactions for device updates with source-tracking."""
|
||||
|
||||
def setUp(self):
|
||||
"""Create an in-memory SQLite DB for testing."""
|
||||
self.test_db = tempfile.NamedTemporaryFile(delete=False, suffix='.db')
|
||||
self.test_db_path = self.test_db.name
|
||||
self.test_db.close()
|
||||
|
||||
# Create minimal schema
|
||||
conn = sqlite3.connect(self.test_db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
|
||||
# Create Devices table with source-tracking columns
|
||||
cur.execute("""
|
||||
CREATE TABLE Devices (
|
||||
devMac TEXT PRIMARY KEY,
|
||||
devName TEXT,
|
||||
devOwner TEXT,
|
||||
devType TEXT,
|
||||
devVendor TEXT,
|
||||
devIcon TEXT,
|
||||
devFavorite INTEGER DEFAULT 0,
|
||||
devGroup TEXT,
|
||||
devLocation TEXT,
|
||||
devComments TEXT,
|
||||
devParentMAC TEXT,
|
||||
devParentPort TEXT,
|
||||
devSSID TEXT,
|
||||
devSite TEXT,
|
||||
devStaticIP INTEGER DEFAULT 0,
|
||||
devScan INTEGER DEFAULT 0,
|
||||
devAlertEvents INTEGER DEFAULT 0,
|
||||
devAlertDown INTEGER DEFAULT 0,
|
||||
devParentRelType TEXT DEFAULT 'default',
|
||||
devReqNicsOnline INTEGER DEFAULT 0,
|
||||
devSkipRepeated INTEGER DEFAULT 0,
|
||||
devIsNew INTEGER DEFAULT 0,
|
||||
devIsArchived INTEGER DEFAULT 0,
|
||||
devLastConnection TEXT,
|
||||
devFirstConnection TEXT,
|
||||
devLastIP TEXT,
|
||||
devGUID TEXT,
|
||||
devCustomProps TEXT,
|
||||
devSourcePlugin TEXT,
|
||||
devNameSource TEXT,
|
||||
devTypeSource TEXT,
|
||||
devVendorSource TEXT,
|
||||
devIconSource TEXT,
|
||||
devGroupSource TEXT,
|
||||
devLocationSource TEXT,
|
||||
devCommentsSource TEXT,
|
||||
devMacSource TEXT
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test database."""
|
||||
if os.path.exists(self.test_db_path):
|
||||
os.unlink(self.test_db_path)
|
||||
|
||||
def _get_test_db_connection(self):
|
||||
"""Override database connection for testing."""
|
||||
conn = sqlite3.connect(self.test_db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def test_create_new_device_atomicity(self):
|
||||
"""
|
||||
Test that device creation and source-tracking are atomic.
|
||||
If source tracking fails, the device should not be created.
|
||||
"""
|
||||
device_instance = DeviceInstance()
|
||||
test_mac = normalize_mac("aa:bb:cc:dd:ee:ff")
|
||||
|
||||
# Patch at module level where it's used
|
||||
with patch('models.device_instance.get_temp_db_connection', self._get_test_db_connection):
|
||||
# Create a new device
|
||||
data = {
|
||||
"createNew": True,
|
||||
"devMac": test_mac,
|
||||
"devName": "Test Device",
|
||||
"devOwner": "John Doe",
|
||||
"devType": "Laptop",
|
||||
}
|
||||
|
||||
result = device_instance.setDeviceData(test_mac, data)
|
||||
|
||||
# Verify success
|
||||
self.assertTrue(result["success"], f"Device creation failed: {result}")
|
||||
|
||||
# Verify device exists
|
||||
conn = self._get_test_db_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM Devices WHERE devMac = ?", (test_mac,))
|
||||
device = cur.fetchone()
|
||||
conn.close()
|
||||
|
||||
self.assertIsNotNone(device, "Device was not created")
|
||||
self.assertEqual(device["devName"], "Test Device")
|
||||
|
||||
# Verify source tracking was set
|
||||
self.assertEqual(device["devMacSource"], "NEWDEV")
|
||||
self.assertEqual(device["devNameSource"], "NEWDEV")
|
||||
|
||||
def test_update_device_with_source_tracking_atomicity(self):
|
||||
"""
|
||||
Test that device update and source-tracking are atomic.
|
||||
If source tracking fails, the device update should be rolled back.
|
||||
"""
|
||||
device_instance = DeviceInstance()
|
||||
test_mac = normalize_mac("aa:bb:cc:dd:ee:ff")
|
||||
|
||||
# Create initial device
|
||||
conn = self._get_test_db_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
INSERT INTO Devices (
|
||||
devMac, devName, devOwner, devType,
|
||||
devNameSource, devTypeSource
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (test_mac, "Old Name", "Old Owner", "Desktop", "PLUGIN", "PLUGIN"))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Patch database connection
|
||||
with patch('models.device_instance.get_temp_db_connection', self._get_test_db_connection):
|
||||
with patch('models.device_instance.enforce_source_on_user_update') as mock_enforce:
|
||||
mock_enforce.return_value = None
|
||||
data = {
|
||||
"createNew": False,
|
||||
"devMac": test_mac,
|
||||
"devName": "New Name",
|
||||
"devOwner": "New Owner",
|
||||
}
|
||||
|
||||
result = device_instance.setDeviceData(test_mac, data)
|
||||
|
||||
# Verify success
|
||||
self.assertTrue(result["success"], f"Device update failed: {result}")
|
||||
|
||||
# Verify device was updated
|
||||
conn = self._get_test_db_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM Devices WHERE devMac = ?", (test_mac,))
|
||||
device = cur.fetchone()
|
||||
conn.close()
|
||||
|
||||
self.assertEqual(device["devName"], "New Name")
|
||||
self.assertEqual(device["devOwner"], "New Owner")
|
||||
|
||||
def test_source_tracking_failure_rolls_back_device(self):
|
||||
"""
|
||||
Test that if enforce_source_on_user_update fails, the entire
|
||||
transaction is rolled back (device and sources).
|
||||
"""
|
||||
device_instance = DeviceInstance()
|
||||
test_mac = normalize_mac("aa:bb:cc:dd:ee:ff")
|
||||
|
||||
# Create initial device
|
||||
conn = self._get_test_db_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
INSERT INTO Devices (
|
||||
devMac, devName, devOwner, devType,
|
||||
devNameSource, devTypeSource
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (test_mac, "Original Name", "Original Owner", "Desktop", "PLUGIN", "PLUGIN"))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Patch database connection and mock source enforcement failure
|
||||
with patch('models.device_instance.get_temp_db_connection', self._get_test_db_connection):
|
||||
with patch('models.device_instance.enforce_source_on_user_update') as mock_enforce:
|
||||
# Simulate source tracking failure
|
||||
mock_enforce.side_effect = Exception("Source tracking error")
|
||||
|
||||
data = {
|
||||
"createNew": False,
|
||||
"devMac": test_mac,
|
||||
"devName": "Failed Update",
|
||||
"devOwner": "Failed Owner",
|
||||
}
|
||||
|
||||
result = device_instance.setDeviceData(test_mac, data)
|
||||
|
||||
# Verify error response
|
||||
self.assertFalse(result["success"])
|
||||
self.assertIn("Source tracking failed", result["error"])
|
||||
|
||||
# Verify device was NOT updated (rollback successful)
|
||||
conn = self._get_test_db_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM Devices WHERE devMac = ?", (test_mac,))
|
||||
device = cur.fetchone()
|
||||
conn.close()
|
||||
|
||||
self.assertEqual(device["devName"], "Original Name", "Device should not have been updated on source tracking failure")
|
||||
self.assertEqual(device["devOwner"], "Original Owner", "Device should not have been updated on source tracking failure")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -16,3 +16,9 @@ def test_normalize_mac_preserves_wildcard():
|
||||
result = normalize_mac("aabbcc*")
|
||||
assert result == "AA:BB:CC:*", f"Expected 'AA:BB:CC:*' but got '{result}'"
|
||||
assert normalize_mac("aa:bb:cc:dd:ee:ff") == "AA:BB:CC:DD:EE:FF"
|
||||
|
||||
|
||||
def test_normalize_mac_preserves_internet_root():
|
||||
assert normalize_mac("internet") == "Internet"
|
||||
assert normalize_mac("Internet") == "Internet"
|
||||
assert normalize_mac("INTERNET") == "Internet"
|
||||
|
||||
Reference in New Issue
Block a user