Refactor event and session column names to camelCase

- Updated test cases to reflect new column names (eve_MAC -> eveMac, eve_DateTime -> eveDateTime, etc.) across various test files.
- Modified SQL table definitions in the database cleanup and migration tests to use camelCase naming conventions.
- Implemented migration tests to ensure legacy column names are correctly renamed to camelCase equivalents.
- Ensured that existing data is preserved during the migration process and that views referencing old column names are dropped before renaming.
- Verified that the migration function is idempotent, allowing for safe re-execution without data loss.
This commit is contained in:
Jokob @NetAlertX
2026-03-16 10:11:22 +00:00
parent 0bb6db155b
commit c7399215ec
109 changed files with 2403 additions and 1967 deletions

View File

@@ -61,7 +61,7 @@ def test_create_event(client, api_token, test_mac):
resp = list_events(client, api_token, test_mac)
assert resp.status_code == 200
events = resp.get_json().get("events", [])
assert any(ev.get("eve_MAC") == test_mac for ev in events)
assert any(ev.get("eveMac") == test_mac for ev in events)
def test_delete_events_for_mac(client, api_token, test_mac):
@@ -73,7 +73,7 @@ def test_delete_events_for_mac(client, api_token, test_mac):
resp = list_events(client, api_token, test_mac)
assert resp.status_code == 200
events = resp.json.get("events", [])
assert any(ev["eve_MAC"] == test_mac for ev in events)
assert any(ev["eveMac"] == test_mac for ev in events)
# delete
resp = client.delete(f"/events/{test_mac}", headers=auth_headers(api_token))
@@ -143,10 +143,10 @@ def test_delete_events_dynamic_days(client, api_token, test_mac):
thirty_days_ago = timeNowUTC(as_string=False) - timedelta(days=30)
initial_younger_count = 0
for ev in initial_events:
if ev.get("eve_MAC") == test_mac and ev.get("eve_DateTime"):
if ev.get("eveMac") == test_mac and ev.get("eveDateTime"):
try:
# Parse event datetime (handle ISO format)
ev_time_str = ev["eve_DateTime"]
ev_time_str = ev["eveDateTime"]
# Try parsing with timezone info
try:
ev_time = datetime.fromisoformat(ev_time_str.replace("Z", "+00:00"))
@@ -176,6 +176,6 @@ def test_delete_events_dynamic_days(client, api_token, test_mac):
# confirm only recent events remain (pre-existing younger + newly created 5-day-old)
resp = list_events(client, api_token, test_mac)
events = resp.get_json().get("events", [])
mac_events = [ev for ev in events if ev.get("eve_MAC") == test_mac]
mac_events = [ev for ev in events if ev.get("eveMac") == test_mac]
expected_remaining = initial_younger_count + 1 # 1 for the 5-day-old event we created
assert len(mac_events) == expected_remaining

View File

@@ -116,7 +116,7 @@ def test_get_open_ports_ip(mock_device_db_conn, mock_plugin_db_conn, client, api
mock_execute_result = MagicMock()
# Mock for PluginObjectInstance.getByField (returns port data)
mock_execute_result.fetchall.return_value = [{"Object_SecondaryID": "22", "Watched_Value2": "ssh"}, {"Object_SecondaryID": "80", "Watched_Value2": "http"}]
mock_execute_result.fetchall.return_value = [{"objectSecondaryId": "22", "watchedValue2": "ssh"}, {"objectSecondaryId": "80", "watchedValue2": "http"}]
# Mock for DeviceInstance.getByIP (returns device with MAC)
mock_execute_result.fetchone.return_value = {"devMac": "aa:bb:cc:dd:ee:ff"}
@@ -141,7 +141,7 @@ def test_get_open_ports_mac_resolve(mock_plugin_db_conn, client, api_token):
# Mock database connection for MAC-based open ports query
mock_conn = MagicMock()
mock_execute_result = MagicMock()
mock_execute_result.fetchall.return_value = [{"Object_SecondaryID": "80", "Watched_Value2": "http"}]
mock_execute_result.fetchall.return_value = [{"objectSecondaryId": "80", "watchedValue2": "http"}]
mock_conn.execute.return_value = mock_execute_result
mock_plugin_db_conn.return_value = mock_conn
@@ -189,7 +189,7 @@ def test_get_recent_alerts(mock_db_conn, client, api_token):
mock_conn = MagicMock()
mock_execute_result = MagicMock()
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
mock_execute_result.fetchall.return_value = [{"eve_DateTime": now, "eve_EventType": "New Device", "eve_MAC": "aa:bb:cc:dd:ee:ff"}]
mock_execute_result.fetchall.return_value = [{"eveDateTime": now, "eveEventType": "New Device", "eveMac": "aa:bb:cc:dd:ee:ff"}]
mock_conn.execute.return_value = mock_execute_result
mock_db_conn.return_value = mock_conn

View File

@@ -74,7 +74,7 @@ def test_list_sessions(client, api_token, test_mac):
assert resp.status_code == 200
assert resp.json.get("success") is True
sessions = resp.json.get("sessions")
assert any(ses["ses_MAC"] == test_mac for ses in sessions)
assert any(ses["sesMac"] == test_mac for ses in sessions)
def test_device_sessions_by_period(client, api_token, test_mac):
@@ -105,7 +105,7 @@ def test_device_sessions_by_period(client, api_token, test_mac):
print(test_mac)
assert isinstance(sessions, list)
assert any(s["ses_MAC"] == test_mac for s in sessions)
assert any(s["sesMac"] == test_mac for s in sessions)
def test_device_session_events(client, api_token, test_mac):
@@ -178,7 +178,7 @@ def test_delete_session(client, api_token, test_mac):
# Confirm deletion
resp = client.get(f"/sessions/list?mac={test_mac}", headers=auth_headers(api_token))
sessions = resp.json.get("sessions")
assert not any(ses["ses_MAC"] == test_mac for ses in sessions)
assert not any(ses["sesMac"] == test_mac for ses in sessions)
def test_get_sessions_calendar(client, api_token, test_mac):

View File

@@ -20,10 +20,10 @@ class SafeConditionBuilder:
# Whitelist of allowed column names for filtering
ALLOWED_COLUMNS = {
"eve_MAC",
"eve_DateTime",
"eve_IP",
"eve_EventType",
"eveMac",
"eveDateTime",
"eveIp",
"eveEventType",
"devName",
"devComments",
"devLastIP",
@@ -34,15 +34,15 @@ class SafeConditionBuilder:
"devPresentLastScan",
"devFavorite",
"devIsNew",
"Plugin",
"Object_PrimaryId",
"Object_SecondaryId",
"DateTimeChanged",
"Watched_Value1",
"Watched_Value2",
"Watched_Value3",
"Watched_Value4",
"Status",
"plugin",
"objectPrimaryId",
"objectSecondaryId",
"dateTimeChanged",
"watchedValue1",
"watchedValue2",
"watchedValue3",
"watchedValue4",
"status",
}
# Whitelist of allowed comparison operators
@@ -403,7 +403,7 @@ class SafeConditionBuilder:
This method handles basic patterns like:
- devName = 'value' (with optional AND/OR prefix)
- devComments LIKE '%value%'
- eve_EventType IN ('type1', 'type2')
- eveEventType IN ('type1', 'type2')
Args:
condition: Single condition string to parse
@@ -633,7 +633,7 @@ class SafeConditionBuilder:
self.parameters[param_name] = event_type
param_names.append(f":{param_name}")
sql_snippet = f"AND eve_EventType IN ({', '.join(param_names)})"
sql_snippet = f"AND eveEventType IN ({', '.join(param_names)})"
return sql_snippet, self.parameters
def get_safe_condition_legacy(

View File

@@ -174,7 +174,7 @@ def test_compound_with_like_patterns(builder):
def test_compound_with_inequality_operators(builder):
"""Test compound conditions with various inequality operators."""
condition = "AND eve_DateTime > '2024-01-01' AND eve_DateTime < '2024-12-31'"
condition = "AND eveDateTime > '2024-01-01' AND eveDateTime < '2024-12-31'"
sql, params = builder.build_safe_condition(condition)

View File

@@ -32,25 +32,25 @@ def _make_json(section, devices, column_names, title="Test Section"):
SAMPLE_NEW_DEVICES = [
{
"devName": "MyPhone",
"eve_MAC": "aa:bb:cc:dd:ee:ff",
"eveMac": "aa:bb:cc:dd:ee:ff",
"devVendor": "",
"eve_IP": "192.168.1.42",
"eve_DateTime": "2025-01-15 10:30:00",
"eve_EventType": "New Device",
"eveIp": "192.168.1.42",
"eveDateTime": "2025-01-15 10:30:00",
"eveEventType": "New Device",
"devComments": "",
},
{
"devName": "Laptop",
"eve_MAC": "11:22:33:44:55:66",
"eveMac": "11:22:33:44:55:66",
"devVendor": "Dell",
"eve_IP": "192.168.1.99",
"eve_DateTime": "2025-01-15 11:00:00",
"eve_EventType": "New Device",
"eveIp": "192.168.1.99",
"eveDateTime": "2025-01-15 11:00:00",
"eveEventType": "New Device",
"devComments": "Office",
},
]
NEW_DEVICE_COLUMNS = ["devName", "eve_MAC", "devVendor", "eve_IP", "eve_DateTime", "eve_EventType", "devComments"]
NEW_DEVICE_COLUMNS = ["devName", "eveMac", "devVendor", "eveIp", "eveDateTime", "eveEventType", "devComments"]
class TestConstructNotificationsTemplates(unittest.TestCase):
@@ -100,7 +100,7 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
self.assertIn("---------", text)
# Legacy format: each header appears as "Header: \tValue"
self.assertIn("eve_MAC:", text)
self.assertIn("eveMac:", text)
self.assertIn("aa:bb:cc:dd:ee:ff", text)
self.assertIn("devName:", text)
self.assertIn("MyPhone", text)
@@ -117,7 +117,7 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
mock_setting.side_effect = self._setting_factory({
"NTFPRCS_TEXT_SECTION_HEADERS": True,
"NTFPRCS_TEXT_TEMPLATE_new_devices": "{devName} ({eve_MAC}) - {eve_IP}",
"NTFPRCS_TEXT_TEMPLATE_new_devices": "{devName} ({eveMac}) - {eveIp}",
})
json_data = _make_json(
@@ -157,7 +157,7 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
mock_setting.side_effect = self._setting_factory({
"NTFPRCS_TEXT_SECTION_HEADERS": False,
"NTFPRCS_TEXT_TEMPLATE_new_devices": "{devName} ({eve_MAC})",
"NTFPRCS_TEXT_TEMPLATE_new_devices": "{devName} ({eveMac})",
})
json_data = _make_json(
@@ -198,7 +198,7 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
mock_setting.side_effect = self._setting_factory({
"NTFPRCS_TEXT_SECTION_HEADERS": True,
"NTFPRCS_TEXT_TEMPLATE_new_devices": "{devName} ({BadField}) - {eve_IP}",
"NTFPRCS_TEXT_TEMPLATE_new_devices": "{devName} ({BadField}) - {eveIp}",
})
json_data = _make_json(
@@ -217,21 +217,21 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
mock_setting.side_effect = self._setting_factory({
"NTFPRCS_TEXT_SECTION_HEADERS": True,
"NTFPRCS_TEXT_TEMPLATE_down_devices": "{devName} ({eve_MAC}) down since {eve_DateTime}",
"NTFPRCS_TEXT_TEMPLATE_down_devices": "{devName} ({eveMac}) down since {eveDateTime}",
})
down_devices = [
{
"devName": "Router",
"eve_MAC": "ff:ee:dd:cc:bb:aa",
"eveMac": "ff:ee:dd:cc:bb:aa",
"devVendor": "Cisco",
"eve_IP": "10.0.0.1",
"eve_DateTime": "2025-01-15 08:00:00",
"eve_EventType": "Device Down",
"eveIp": "10.0.0.1",
"eveDateTime": "2025-01-15 08:00:00",
"eveEventType": "Device Down",
"devComments": "",
}
]
columns = ["devName", "eve_MAC", "devVendor", "eve_IP", "eve_DateTime", "eve_EventType", "devComments"]
columns = ["devName", "eveMac", "devVendor", "eveIp", "eveDateTime", "eveEventType", "devComments"]
json_data = _make_json("down_devices", down_devices, columns, "🔴 Down devices")
_, text = construct_notifications(json_data, "down_devices")

View File

@@ -15,7 +15,7 @@ sys.modules['logger'] = Mock()
class SafeConditionBuilderForTesting:
"""Minimal SafeConditionBuilder implementation for tests."""
ALLOWED_COLUMNS = {'devName', 'eve_MAC', 'eve_EventType'}
ALLOWED_COLUMNS = {'devName', 'eveMac', 'eveEventType'}
ALLOWED_OPERATORS = {'=', '!=', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE'}
ALLOWED_LOGICAL_OPERATORS = {'AND', 'OR'}

View File

@@ -174,13 +174,13 @@ def test_null_byte_injection(builder):
def test_build_condition_with_allowed_values(builder):
"""Test building condition with specific allowed values."""
conditions = [
{"column": "eve_EventType", "operator": "=", "value": "Connected"},
{"column": "eveEventType", "operator": "=", "value": "Connected"},
{"column": "devName", "operator": "LIKE", "value": "%test%"}
]
condition, params = builder.build_condition(conditions, "AND")
# Should create valid parameterized condition
assert "eve_EventType = :" in condition
assert "eveEventType = :" in condition
assert "devName LIKE :" in condition
assert len(params) == 2

View File

@@ -58,9 +58,9 @@ class TestSafeConditionBuilder(unittest.TestCase):
def test_validate_column_name(self):
"""Test column name validation against whitelist."""
# Valid columns
self.assertTrue(self.builder._validate_column_name('eve_MAC'))
self.assertTrue(self.builder._validate_column_name('eveMac'))
self.assertTrue(self.builder._validate_column_name('devName'))
self.assertTrue(self.builder._validate_column_name('eve_EventType'))
self.assertTrue(self.builder._validate_column_name('eveEventType'))
# Invalid columns
self.assertFalse(self.builder._validate_column_name('malicious_column'))
@@ -103,9 +103,9 @@ class TestSafeConditionBuilder(unittest.TestCase):
def test_build_in_condition_valid(self):
"""Test building valid IN conditions."""
sql, params = self.builder._build_in_condition('AND', 'eve_EventType', 'IN', "'Connected', 'Disconnected'")
sql, params = self.builder._build_in_condition('AND', 'eveEventType', 'IN', "'Connected', 'Disconnected'")
self.assertIn('AND eve_EventType IN', sql)
self.assertIn('AND eveEventType IN', sql)
self.assertEqual(len(params), 2)
self.assertIn('Connected', params.values())
self.assertIn('Disconnected', params.values())
@@ -162,7 +162,7 @@ class TestSafeConditionBuilder(unittest.TestCase):
event_types = ['Connected', 'Disconnected']
sql, params = self.builder.build_event_type_filter(event_types)
self.assertIn('AND eve_EventType IN', sql)
self.assertIn('AND eveEventType IN', sql)
self.assertEqual(len(params), 2)
self.assertIn('Connected', params.values())
self.assertIn('Disconnected', params.values())
@@ -354,9 +354,9 @@ class TestSecurityBenchmarks(unittest.TestCase):
"""Test coverage of condition patterns."""
patterns_tested = [
"AND devName = 'value'",
"OR eve_EventType LIKE '%test%'",
"OR eveEventType LIKE '%test%'",
"AND devComments IS NULL",
"AND eve_EventType IN ('Connected', 'Disconnected')",
"AND eveEventType IN ('Connected', 'Disconnected')",
]
for pattern in patterns_tested:

View File

@@ -0,0 +1,307 @@
"""
Unit tests for migrate_to_camelcase() in db_upgrade.
Covers:
- Already-migrated schema (eveMac present) → skip, return True
- Unrecognised schema (neither eveMac nor eve_MAC) → skip, return True
- Legacy Events columns renamed to camelCase equivalents
- Legacy Sessions columns renamed to camelCase equivalents
- Legacy Online_History columns renamed to camelCase equivalents
- Legacy Plugins_Objects columns renamed to camelCase equivalents
- Legacy Plugins_Language_Strings columns renamed to camelCase equivalents
- Missing tables are silently skipped without error
- Existing row data is preserved through the column rename
- Views referencing old column names are dropped before ALTER TABLE runs
- Migration is idempotent (second call detects eveMac and returns early)
"""
import sys
import os
import sqlite3
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from db.db_upgrade import migrate_to_camelcase # noqa: E402
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_cursor():
"""Return an in-memory SQLite cursor and its parent connection."""
conn = sqlite3.connect(":memory:")
return conn.cursor(), conn
def _col_names(cursor, table):
"""Return the set of column names for a given table."""
cursor.execute(f'PRAGMA table_info("{table}")')
return {row[1] for row in cursor.fetchall()}
# ---------------------------------------------------------------------------
# Legacy DDL fixtures (pre-migration schema with old column names)
# ---------------------------------------------------------------------------
_LEGACY_EVENTS_DDL = """
CREATE TABLE Events (
eve_MAC TEXT NOT NULL,
eve_IP TEXT NOT NULL,
eve_DateTime DATETIME NOT NULL,
eve_EventType TEXT NOT NULL,
eve_AdditionalInfo TEXT DEFAULT '',
eve_PendingAlertEmail INTEGER NOT NULL DEFAULT 1,
eve_PairEventRowid INTEGER
)
"""
_LEGACY_SESSIONS_DDL = """
CREATE TABLE Sessions (
ses_MAC TEXT,
ses_IP TEXT,
ses_EventTypeConnection TEXT,
ses_DateTimeConnection DATETIME,
ses_EventTypeDisconnection TEXT,
ses_DateTimeDisconnection DATETIME,
ses_StillConnected INTEGER,
ses_AdditionalInfo TEXT
)
"""
_LEGACY_ONLINE_HISTORY_DDL = """
CREATE TABLE Online_History (
"Index" INTEGER PRIMARY KEY AUTOINCREMENT,
"Scan_Date" TEXT,
"Online_Devices" INTEGER,
"Down_Devices" INTEGER,
"All_Devices" INTEGER,
"Archived_Devices" INTEGER,
"Offline_Devices" INTEGER
)
"""
_LEGACY_PLUGINS_OBJECTS_DDL = """
CREATE TABLE Plugins_Objects (
"Index" INTEGER PRIMARY KEY AUTOINCREMENT,
Plugin TEXT NOT NULL,
Object_PrimaryID TEXT NOT NULL,
Object_SecondaryID TEXT NOT NULL,
DateTimeCreated TEXT NOT NULL,
DateTimeChanged TEXT NOT NULL,
Watched_Value1 TEXT NOT NULL,
Watched_Value2 TEXT NOT NULL,
Watched_Value3 TEXT NOT NULL,
Watched_Value4 TEXT NOT NULL,
Status TEXT NOT NULL,
Extra TEXT NOT NULL,
UserData TEXT NOT NULL,
ForeignKey TEXT NOT NULL,
SyncHubNodeName TEXT,
HelpVal1 TEXT,
HelpVal2 TEXT,
HelpVal3 TEXT,
HelpVal4 TEXT,
ObjectGUID TEXT
)
"""
_LEGACY_PLUGINS_LANG_DDL = """
CREATE TABLE Plugins_Language_Strings (
"Index" INTEGER PRIMARY KEY AUTOINCREMENT,
Language_Code TEXT NOT NULL,
String_Key TEXT NOT NULL,
String_Value TEXT NOT NULL,
Extra TEXT NOT NULL
)
"""
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestMigrateToCamelCase:
def test_returns_true_if_already_camelcase(self):
"""DB already on camelCase schema → skip silently, return True."""
cur, conn = _make_cursor()
cur.execute("""
CREATE TABLE Events (
eveMac TEXT NOT NULL, eveIp TEXT NOT NULL,
eveDateTime DATETIME NOT NULL, eveEventType TEXT NOT NULL,
eveAdditionalInfo TEXT, evePendingAlertEmail INTEGER,
evePairEventRowid INTEGER
)
""")
conn.commit()
result = migrate_to_camelcase(cur)
assert result is True
assert "eveMac" in _col_names(cur, "Events")
def test_returns_true_if_unknown_schema(self):
"""Events exists but has neither eve_MAC nor eveMac → skip, return True."""
cur, conn = _make_cursor()
cur.execute("CREATE TABLE Events (someOtherCol TEXT)")
conn.commit()
result = migrate_to_camelcase(cur)
assert result is True
def test_events_legacy_columns_renamed(self):
"""All legacy eve_* columns are renamed to their camelCase equivalents."""
cur, conn = _make_cursor()
cur.execute(_LEGACY_EVENTS_DDL)
conn.commit()
result = migrate_to_camelcase(cur)
assert result is True
cols = _col_names(cur, "Events")
expected_new = {
"eveMac", "eveIp", "eveDateTime", "eveEventType",
"eveAdditionalInfo", "evePendingAlertEmail", "evePairEventRowid",
}
old_names = {
"eve_MAC", "eve_IP", "eve_DateTime", "eve_EventType",
"eve_AdditionalInfo", "eve_PendingAlertEmail", "eve_PairEventRowid",
}
assert expected_new.issubset(cols), f"Missing new columns: {expected_new - cols}"
assert not old_names & cols, f"Old columns still present: {old_names & cols}"
def test_sessions_legacy_columns_renamed(self):
"""All legacy ses_* columns are renamed to their camelCase equivalents."""
cur, conn = _make_cursor()
cur.execute(_LEGACY_EVENTS_DDL)
cur.execute(_LEGACY_SESSIONS_DDL)
conn.commit()
result = migrate_to_camelcase(cur)
assert result is True
cols = _col_names(cur, "Sessions")
assert {
"sesMac", "sesIp", "sesEventTypeConnection", "sesDateTimeConnection",
"sesEventTypeDisconnection", "sesDateTimeDisconnection",
"sesStillConnected", "sesAdditionalInfo",
}.issubset(cols)
assert not {"ses_MAC", "ses_IP", "ses_DateTimeConnection"} & cols
def test_online_history_legacy_columns_renamed(self):
"""Quoted legacy Online_History column names are renamed to camelCase."""
cur, conn = _make_cursor()
cur.execute(_LEGACY_EVENTS_DDL)
cur.execute(_LEGACY_ONLINE_HISTORY_DDL)
conn.commit()
migrate_to_camelcase(cur)
cols = _col_names(cur, "Online_History")
assert {
"scanDate", "onlineDevices", "downDevices",
"allDevices", "archivedDevices", "offlineDevices",
}.issubset(cols)
assert not {"Scan_Date", "Online_Devices", "Down_Devices"} & cols
def test_plugins_objects_legacy_columns_renamed(self):
"""All renamed Plugins_Objects columns receive their camelCase names."""
cur, conn = _make_cursor()
cur.execute(_LEGACY_EVENTS_DDL)
cur.execute(_LEGACY_PLUGINS_OBJECTS_DDL)
conn.commit()
migrate_to_camelcase(cur)
cols = _col_names(cur, "Plugins_Objects")
assert {
"plugin", "objectPrimaryId", "objectSecondaryId",
"dateTimeCreated", "dateTimeChanged",
"watchedValue1", "watchedValue2", "watchedValue3", "watchedValue4",
"status", "extra", "userData", "foreignKey", "syncHubNodeName",
"helpVal1", "helpVal2", "helpVal3", "helpVal4", "objectGuid",
}.issubset(cols)
assert not {
"Object_PrimaryID", "Watched_Value1", "ObjectGUID",
"ForeignKey", "UserData", "Plugin",
} & cols
def test_plugins_language_strings_renamed(self):
"""Plugins_Language_Strings legacy column names are renamed to camelCase."""
cur, conn = _make_cursor()
cur.execute(_LEGACY_EVENTS_DDL)
cur.execute(_LEGACY_PLUGINS_LANG_DDL)
conn.commit()
migrate_to_camelcase(cur)
cols = _col_names(cur, "Plugins_Language_Strings")
assert {"languageCode", "stringKey", "stringValue", "extra"}.issubset(cols)
assert not {"Language_Code", "String_Key", "String_Value"} & cols
def test_missing_table_silently_skipped(self):
"""Tables in the migration map that don't exist are skipped without error."""
cur, conn = _make_cursor()
# Only Events (legacy) exists — all other mapped tables are absent
cur.execute(_LEGACY_EVENTS_DDL)
conn.commit()
result = migrate_to_camelcase(cur)
assert result is True
assert "eveMac" in _col_names(cur, "Events")
def test_data_preserved_after_rename(self):
"""Existing rows remain accessible under the new camelCase column names."""
cur, conn = _make_cursor()
cur.execute(_LEGACY_EVENTS_DDL)
cur.execute(
"INSERT INTO Events (eve_MAC, eve_IP, eve_DateTime, eve_EventType) "
"VALUES ('aa:bb:cc:dd:ee:ff', '192.168.1.1', '2025-01-01 12:00:00', 'Connected')"
)
conn.commit()
migrate_to_camelcase(cur)
cur.execute(
"SELECT eveMac, eveIp, eveEventType FROM Events WHERE eveMac = 'aa:bb:cc:dd:ee:ff'"
)
row = cur.fetchone()
assert row is not None, "Row missing after camelCase migration"
assert row[0] == "aa:bb:cc:dd:ee:ff"
assert row[1] == "192.168.1.1"
assert row[2] == "Connected"
def test_views_dropped_before_migration(self):
"""Views referencing old column names do not block ALTER TABLE RENAME COLUMN."""
cur, conn = _make_cursor()
cur.execute(_LEGACY_EVENTS_DDL)
# A view that references old column names would normally block the rename
cur.execute("CREATE VIEW Events_Devices AS SELECT eve_MAC, eve_IP FROM Events")
conn.commit()
result = migrate_to_camelcase(cur)
assert result is True
assert "eveMac" in _col_names(cur, "Events")
# View is dropped (ensure_views() is responsible for recreation separately)
cur.execute("SELECT name FROM sqlite_master WHERE type='view' AND name='Events_Devices'")
assert cur.fetchone() is None
def test_idempotent_second_run(self):
"""Running migration twice is safe — second call detects eveMac and exits early."""
cur, conn = _make_cursor()
cur.execute(_LEGACY_EVENTS_DDL)
conn.commit()
first = migrate_to_camelcase(cur)
second = migrate_to_camelcase(cur)
assert first is True
assert second is True
cols = _col_names(cur, "Events")
assert "eveMac" in cols
assert "eve_MAC" not in cols

View File

@@ -25,26 +25,26 @@ def _make_db():
cur.execute("""
CREATE TABLE Events (
eve_MAC TEXT NOT NULL,
eve_IP TEXT NOT NULL,
eve_DateTime DATETIME NOT NULL,
eve_EventType TEXT NOT NULL,
eve_AdditionalInfo TEXT DEFAULT '',
eve_PendingAlertEmail INTEGER NOT NULL DEFAULT 1,
eve_PairEventRowid INTEGER
eveMac TEXT NOT NULL,
eveIp TEXT NOT NULL,
eveDateTime DATETIME NOT NULL,
eveEventType TEXT NOT NULL,
eveAdditionalInfo TEXT DEFAULT '',
evePendingAlertEmail INTEGER NOT NULL DEFAULT 1,
evePairEventRowid INTEGER
)
""")
cur.execute("""
CREATE TABLE Sessions (
ses_MAC TEXT,
ses_IP TEXT,
ses_EventTypeConnection TEXT,
ses_DateTimeConnection DATETIME,
ses_EventTypeDisconnection TEXT,
ses_DateTimeDisconnection DATETIME,
ses_StillConnected INTEGER,
ses_AdditionalInfo TEXT
sesMac TEXT,
sesIp TEXT,
sesEventTypeConnection TEXT,
sesDateTimeConnection DATETIME,
sesEventTypeDisconnection TEXT,
sesDateTimeDisconnection DATETIME,
sesStillConnected INTEGER,
sesAdditionalInfo TEXT
)
""")
@@ -59,13 +59,13 @@ def _seed_sessions(cur, old_count: int, recent_count: int, days: int):
"""
for i in range(old_count):
cur.execute(
"INSERT INTO Sessions (ses_MAC, ses_DateTimeConnection) "
"INSERT INTO Sessions (sesMac, sesDateTimeConnection) "
"VALUES (?, date('now', ?))",
(f"AA:BB:CC:DD:EE:{i:02X}", f"-{days + 1} day"),
)
for i in range(recent_count):
cur.execute(
"INSERT INTO Sessions (ses_MAC, ses_DateTimeConnection) "
"INSERT INTO Sessions (sesMac, sesDateTimeConnection) "
"VALUES (?, date('now'))",
(f"11:22:33:44:55:{i:02X}",),
)
@@ -75,7 +75,7 @@ def _run_sessions_trim(cur, days: int) -> int:
"""Execute the exact DELETE used by db_cleanup and return rowcount."""
cur.execute(
f"DELETE FROM Sessions "
f"WHERE ses_DateTimeConnection <= date('now', '-{days} day')"
f"WHERE sesDateTimeConnection <= date('now', '-{days} day')"
)
return cur.rowcount
@@ -126,20 +126,20 @@ class TestSessionsTrim:
cur = conn.cursor()
# Row exactly AT the boundary (date = 'now' - days exactly)
cur.execute(
"INSERT INTO Sessions (ses_MAC, ses_DateTimeConnection) "
"INSERT INTO Sessions (sesMac, sesDateTimeConnection) "
"VALUES (?, date('now', ?))",
("AA:BB:CC:00:00:01", "-30 day"),
)
# Row just inside the window
cur.execute(
"INSERT INTO Sessions (ses_MAC, ses_DateTimeConnection) "
"INSERT INTO Sessions (sesMac, sesDateTimeConnection) "
"VALUES (?, date('now', '-29 day'))",
("AA:BB:CC:00:00:02",),
)
_run_sessions_trim(cur, days=30)
cur.execute("SELECT ses_MAC FROM Sessions")
cur.execute("SELECT sesMac FROM Sessions")
remaining_macs = {row[0] for row in cur.fetchall()}
# Boundary row (== threshold) is deleted; inside row survives
assert "AA:BB:CC:00:00:02" in remaining_macs, "Row inside window was wrongly deleted"
@@ -157,8 +157,8 @@ class TestSessionsTrim:
with open(script_path) as fh:
source = fh.read()
events_expr = "DELETE FROM Events WHERE eve_DateTime <= date('now', '-{str(DAYS_TO_KEEP_EVENTS)} day')"
sessions_expr = "DELETE FROM Sessions WHERE ses_DateTimeConnection <= date('now', '-{str(DAYS_TO_KEEP_EVENTS)} day')"
events_expr = "DELETE FROM Events WHERE eveDateTime <= date('now', '-{str(DAYS_TO_KEEP_EVENTS)} day')"
sessions_expr = "DELETE FROM Sessions WHERE sesDateTimeConnection <= date('now', '-{str(DAYS_TO_KEEP_EVENTS)} day')"
assert events_expr in source, "Events DELETE expression changed unexpectedly"
assert sessions_expr in source, "Sessions DELETE is not aligned with Events DELETE"
@@ -181,7 +181,7 @@ class TestAnalyze:
# Seed some rows so ANALYZE has something to measure
for i in range(20):
cur.execute(
"INSERT INTO Events (eve_MAC, eve_IP, eve_DateTime, eve_EventType) "
"INSERT INTO Events (eveMac, eveIp, eveDateTime, eveEventType) "
"VALUES (?, '1.2.3.4', date('now'), 'Connected')",
(f"AA:BB:CC:DD:EE:{i:02X}",),
)
@@ -238,7 +238,7 @@ class TestPragmaOptimize:
for i in range(50):
cur.execute(
"INSERT INTO Sessions (ses_MAC, ses_DateTimeConnection) "
"INSERT INTO Sessions (sesMac, sesDateTimeConnection) "
"VALUES (?, date('now', '-60 day'))",
(f"AA:BB:CC:DD:EE:{i:02X}",),
)
@@ -247,7 +247,7 @@ class TestPragmaOptimize:
# Mirror the tail sequence from cleanup_database.
# WAL checkpoints are omitted: they require no open transaction and are
# not supported on :memory: databases (SQLite raises OperationalError).
cur.execute("DELETE FROM Sessions WHERE ses_DateTimeConnection <= date('now', '-30 day')")
cur.execute("DELETE FROM Sessions WHERE sesDateTimeConnection <= date('now', '-30 day')")
conn.commit()
cur.execute("ANALYZE;")
conn.execute("VACUUM;")

View File

@@ -76,16 +76,16 @@ CREATE_DEVICES = """
)
"""
# Includes eve_PairEventRowid — required by insert_events().
# Includes evePairEventRowid — required by insert_events().
CREATE_EVENTS = """
CREATE TABLE IF NOT EXISTS Events (
eve_MAC TEXT,
eve_IP TEXT,
eve_DateTime TEXT,
eve_EventType TEXT,
eve_AdditionalInfo TEXT,
eve_PendingAlertEmail INTEGER,
eve_PairEventRowid INTEGER
eveMac TEXT,
eveIp TEXT,
eveDateTime TEXT,
eveEventType TEXT,
eveAdditionalInfo TEXT,
evePendingAlertEmail INTEGER,
evePairEventRowid INTEGER
)
"""
@@ -327,8 +327,8 @@ def sync_insert_devices(
def down_event_macs(cur) -> set:
"""Return the set of MACs that have a 'Device Down' event row (lowercased)."""
cur.execute("SELECT eve_MAC FROM Events WHERE eve_EventType = 'Device Down'")
return {r["eve_MAC"].lower() for r in cur.fetchall()}
cur.execute("SELECT eveMac FROM Events WHERE eveEventType = 'Device Down'")
return {r["eveMac"].lower() for r in cur.fetchall()}
# ---------------------------------------------------------------------------

View File

@@ -39,15 +39,15 @@ def test_db(test_db_path):
# Minimal schema for integration testing
cur.execute('''
CREATE TABLE IF NOT EXISTS Events_Devices (
eve_MAC TEXT,
eve_DateTime TEXT,
eveMac TEXT,
eveDateTime TEXT,
devLastIP TEXT,
eve_IP TEXT,
eve_EventType TEXT,
eveIp TEXT,
eveEventType TEXT,
devName TEXT,
devVendor TEXT,
devComments TEXT,
eve_PendingAlertEmail INTEGER
evePendingAlertEmail INTEGER
)
''')
@@ -63,24 +63,24 @@ def test_db(test_db_path):
cur.execute('''
CREATE TABLE IF NOT EXISTS Events (
eve_MAC TEXT,
eve_DateTime TEXT,
eve_EventType TEXT,
eve_PendingAlertEmail INTEGER
eveMac TEXT,
eveDateTime TEXT,
eveEventType TEXT,
evePendingAlertEmail INTEGER
)
''')
cur.execute('''
CREATE TABLE IF NOT EXISTS Plugins_Events (
Plugin TEXT,
Object_PrimaryId TEXT,
Object_SecondaryId TEXT,
DateTimeChanged TEXT,
Watched_Value1 TEXT,
Watched_Value2 TEXT,
Watched_Value3 TEXT,
Watched_Value4 TEXT,
Status TEXT
plugin TEXT,
objectPrimaryId TEXT,
objectSecondaryId TEXT,
dateTimeChanged TEXT,
watchedValue1 TEXT,
watchedValue2 TEXT,
watchedValue3 TEXT,
watchedValue4 TEXT,
"status" TEXT
)
''')
@@ -91,7 +91,7 @@ def test_db(test_db_path):
('77:88:99:aa:bb:cc', '2024-01-01 12:02:00', '192.168.1.102', '192.168.1.102', 'Disconnected', 'Test Device 3', 'Cisco', 'Third Comment', 1),
]
cur.executemany('''
INSERT INTO Events_Devices (eve_MAC, eve_DateTime, devLastIP, eve_IP, eve_EventType, devName, devVendor, devComments, eve_PendingAlertEmail)
INSERT INTO Events_Devices (eveMac, eveDateTime, devLastIP, eveIp, eveEventType, devName, devVendor, devComments, evePendingAlertEmail)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', test_data)
@@ -117,7 +117,7 @@ def test_fresh_install_compatibility(builder):
def test_existing_db_compatibility():
mock_db = Mock()
mock_result = Mock()
mock_result.columnNames = ['devName', 'eve_MAC', 'devVendor', 'eve_IP', 'eve_DateTime', 'eve_EventType', 'devComments']
mock_result.columnNames = ['devName', 'eveMac', 'devVendor', 'eveIp', 'eveDateTime', 'eveEventType', 'devComments']
mock_result.json = {'data': []}
mock_db.get_table_as_json.return_value = mock_result
@@ -145,9 +145,9 @@ def test_notification_system_integration(builder):
assert "devName = :" in condition
assert 'EmailTestDevice' in params.values()
apprise_condition = "AND eve_EventType = 'Connected'"
apprise_condition = "AND eveEventType = 'Connected'"
condition, params = builder.get_safe_condition_legacy(apprise_condition)
assert "eve_EventType = :" in condition
assert "eveEventType = :" in condition
assert 'Connected' in params.values()
webhook_condition = "AND devComments LIKE '%webhook%'"
@@ -155,9 +155,9 @@ def test_notification_system_integration(builder):
assert "devComments LIKE :" in condition
assert '%webhook%' in params.values()
mqtt_condition = "AND eve_MAC = 'aa:bb:cc:dd:ee:ff'"
mqtt_condition = "AND eveMac = 'aa:bb:cc:dd:ee:ff'"
condition, params = builder.get_safe_condition_legacy(mqtt_condition)
assert "eve_MAC = :" in condition
assert "eveMac = :" in condition
assert 'aa:bb:cc:dd:ee:ff' in params.values()
@@ -165,7 +165,7 @@ def test_settings_persistence(builder):
test_settings = [
"AND devName = 'Persistent Device'",
"AND devComments = {s-quote}Legacy Quote{s-quote}",
"AND eve_EventType IN ('Connected', 'Disconnected')",
"AND eveEventType IN ('Connected', 'Disconnected')",
"AND devLastIP = '192.168.1.1'",
""
]
@@ -190,9 +190,9 @@ def test_device_operations(builder):
def test_plugin_functionality(builder):
plugin_conditions = [
"AND Plugin = 'TestPlugin'",
"AND Object_PrimaryId = 'primary123'",
"AND Status = 'Active'"
"AND plugin = 'TestPlugin'",
"AND objectPrimaryId = 'primary123'",
"AND status = 'Active'"
]
for cond in plugin_conditions:
safe_condition, params = builder.get_safe_condition_legacy(cond)

View File

@@ -258,8 +258,8 @@ class TestInsertEventsSleepSuppression:
can_sleep=1, last_connection=last_conn)
# Simulate: a Device Down event already exists for this absence
cur.execute(
"INSERT INTO Events (eve_MAC, eve_IP, eve_DateTime, eve_EventType, "
"eve_AdditionalInfo, eve_PendingAlertEmail) "
"INSERT INTO Events (eveMac, eveIp, eveDateTime, eveEventType, "
"eveAdditionalInfo, evePendingAlertEmail) "
"VALUES (?, '192.168.1.1', ?, 'Device Down', '', 1)",
("bb:00:00:00:00:04", _minutes_ago(15)),
)
@@ -269,7 +269,7 @@ class TestInsertEventsSleepSuppression:
cur.execute(
"SELECT COUNT(*) as cnt FROM Events "
"WHERE eve_MAC = 'bb:00:00:00:00:04' AND eve_EventType = 'Device Down'"
"WHERE eveMac = 'bb:00:00:00:00:04' AND eveEventType = 'Device Down'"
)
count = cur.fetchone()["cnt"]
assert count == 1, (

View File

@@ -117,12 +117,12 @@ def scan_db_for_new_devices():
cur.execute(
"""
CREATE TABLE Events (
eve_MAC TEXT,
eve_IP TEXT,
eve_DateTime TEXT,
eve_EventType TEXT,
eve_AdditionalInfo TEXT,
eve_PendingAlertEmail INTEGER
eveMac TEXT,
eveIp TEXT,
eveDateTime TEXT,
eveEventType TEXT,
eveAdditionalInfo TEXT,
evePendingAlertEmail INTEGER
)
"""
)
@@ -130,14 +130,14 @@ def scan_db_for_new_devices():
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
sesMac TEXT,
sesIp TEXT,
sesEventTypeConnection TEXT,
sesDateTimeConnection TEXT,
sesEventTypeDisconnection TEXT,
sesDateTimeDisconnection TEXT,
sesStillConnected INTEGER,
sesAdditionalInfo TEXT
)
"""
)