mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-30 23:03:03 -07:00
Merge branch 'main' of https://github.com/jokob-sk/NetAlertX
This commit is contained in:
@@ -8,7 +8,7 @@ INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
|
||||
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ def b64(sql: str) -> str:
|
||||
# -----------------------------
|
||||
def test_dbquery_create_device(client, api_token, test_mac):
|
||||
|
||||
now = timeNowDB()
|
||||
now = timeNowUTC()
|
||||
|
||||
sql = f"""
|
||||
INSERT INTO Devices (devMac, devName, devVendor, devOwner, devFirstConnection, devLastConnection, devLastIP)
|
||||
|
||||
@@ -8,7 +8,7 @@ INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from utils.datetime_utils import timeNowTZ # noqa: E402 [flake8 lint suppression]
|
||||
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def create_event(client, api_token, mac, event="UnitTest Event", days_old=None):
|
||||
|
||||
# Calculate the event_time if days_old is given
|
||||
if days_old is not None:
|
||||
event_time = timeNowTZ() - timedelta(days=days_old)
|
||||
event_time = timeNowUTC(as_string=False) - timedelta(days=days_old)
|
||||
# ISO 8601 string
|
||||
payload["event_time"] = event_time.isoformat()
|
||||
|
||||
@@ -140,7 +140,7 @@ def test_delete_events_dynamic_days(client, api_token, test_mac):
|
||||
# Count pre-existing events younger than 30 days for test_mac
|
||||
# These will remain after delete operation
|
||||
from datetime import datetime
|
||||
thirty_days_ago = timeNowTZ() - timedelta(days=30)
|
||||
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"):
|
||||
|
||||
@@ -8,7 +8,7 @@ INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from utils.datetime_utils import timeNowTZ, timeNowDB # noqa: E402 [flake8 lint suppression]
|
||||
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ def test_create_session(client, api_token, test_mac):
|
||||
payload = {
|
||||
"mac": test_mac,
|
||||
"ip": "192.168.1.100",
|
||||
"start_time": timeNowDB(),
|
||||
"start_time": timeNowUTC(),
|
||||
"event_type_conn": "Connected",
|
||||
"event_type_disc": "Disconnected"
|
||||
}
|
||||
@@ -65,7 +65,7 @@ def test_list_sessions(client, api_token, test_mac):
|
||||
payload = {
|
||||
"mac": test_mac,
|
||||
"ip": "192.168.1.100",
|
||||
"start_time": timeNowDB()
|
||||
"start_time": timeNowUTC()
|
||||
}
|
||||
client.post("/sessions/create", json=payload, headers=auth_headers(api_token))
|
||||
|
||||
@@ -82,7 +82,7 @@ def test_device_sessions_by_period(client, api_token, test_mac):
|
||||
payload = {
|
||||
"mac": test_mac,
|
||||
"ip": "192.168.1.200",
|
||||
"start_time": timeNowDB()
|
||||
"start_time": timeNowUTC()
|
||||
}
|
||||
resp_create = client.post("/sessions/create", json=payload, headers=auth_headers(api_token))
|
||||
assert resp_create.status_code == 200
|
||||
@@ -117,7 +117,7 @@ def test_device_session_events(client, api_token, test_mac):
|
||||
payload = {
|
||||
"mac": test_mac,
|
||||
"ip": "192.168.1.250",
|
||||
"start_time": timeNowDB()
|
||||
"start_time": timeNowUTC()
|
||||
}
|
||||
resp_create = client.post(
|
||||
"/sessions/create",
|
||||
@@ -166,7 +166,7 @@ def test_delete_session(client, api_token, test_mac):
|
||||
payload = {
|
||||
"mac": test_mac,
|
||||
"ip": "192.168.1.100",
|
||||
"start_time": timeNowDB()
|
||||
"start_time": timeNowUTC()
|
||||
}
|
||||
client.post("/sessions/create", json=payload, headers=auth_headers(api_token))
|
||||
|
||||
@@ -188,7 +188,7 @@ def test_get_sessions_calendar(client, api_token, test_mac):
|
||||
Cleans up test sessions after test.
|
||||
"""
|
||||
# --- Setup: create two sessions for the test MAC ---
|
||||
now = timeNowTZ()
|
||||
now = timeNowUTC(as_string=False)
|
||||
start1 = (now - timedelta(days=2)).isoformat(timespec="seconds")
|
||||
end1 = (now - timedelta(days=1, hours=20)).isoformat(timespec="seconds")
|
||||
|
||||
|
||||
238
test/db/test_timestamp_migration.py
Normal file
238
test/db/test_timestamp_migration.py
Normal file
@@ -0,0 +1,238 @@
|
||||
"""
|
||||
Unit tests for database timestamp migration to UTC.
|
||||
|
||||
Tests verify that:
|
||||
- Migration detects version correctly from Settings table
|
||||
- Fresh installs skip migration (empty VERSION)
|
||||
- Upgrades from v26.2.6+ skip migration (already UTC)
|
||||
- Upgrades from <v26.2.6 run migration (convert local→UTC)
|
||||
- Migration handles timezone offset calculations correctly
|
||||
- Migration is idempotent (safe to run multiple times)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
import sqlite3
|
||||
import tempfile
|
||||
|
||||
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_timestamps_to_utc, is_timestamps_in_utc # noqa: E402
|
||||
from utils.datetime_utils import timeNowUTC # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_db():
|
||||
"""Create a temporary database for testing"""
|
||||
fd, db_path = tempfile.mkstemp(suffix='.db')
|
||||
os.close(fd)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create Settings table
|
||||
cursor.execute("""
|
||||
CREATE TABLE Settings (
|
||||
setKey TEXT PRIMARY KEY,
|
||||
setValue TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
# Create Devices table with timestamp columns
|
||||
cursor.execute("""
|
||||
CREATE TABLE Devices (
|
||||
devMac TEXT PRIMARY KEY,
|
||||
devFirstConnection TEXT,
|
||||
devLastConnection TEXT,
|
||||
devLastNotification TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
|
||||
yield cursor, conn
|
||||
|
||||
conn.close()
|
||||
os.unlink(db_path)
|
||||
|
||||
|
||||
class TestTimestampMigration:
|
||||
"""Test suite for UTC timestamp migration"""
|
||||
|
||||
def test_migrate_fresh_install_skips_migration(self, temp_db):
|
||||
"""Test that fresh install with empty VERSION skips migration"""
|
||||
cursor, conn = temp_db
|
||||
|
||||
# Empty Settings table (fresh install)
|
||||
result = migrate_timestamps_to_utc(cursor)
|
||||
|
||||
assert result is True
|
||||
# Should return without error
|
||||
|
||||
def test_migrate_unknown_version_skips_migration(self, temp_db):
|
||||
"""Test that 'unknown' VERSION skips migration"""
|
||||
cursor, conn = temp_db
|
||||
|
||||
# Insert 'unknown' VERSION
|
||||
cursor.execute("INSERT INTO Settings (setKey, setValue) VALUES ('VERSION', 'unknown')")
|
||||
conn.commit()
|
||||
|
||||
result = migrate_timestamps_to_utc(cursor)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_migrate_version_26_2_6_skips_migration(self, temp_db):
|
||||
"""Test that v26.2.6 skips migration (already UTC)"""
|
||||
cursor, conn = temp_db
|
||||
|
||||
# Insert VERSION v26.2.6
|
||||
cursor.execute("INSERT INTO Settings (setKey, setValue) VALUES ('VERSION', '26.2.6')")
|
||||
conn.commit()
|
||||
|
||||
result = migrate_timestamps_to_utc(cursor)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_migrate_version_27_0_0_skips_migration(self, temp_db):
|
||||
"""Test that v27.0.0 skips migration (newer version)"""
|
||||
cursor, conn = temp_db
|
||||
|
||||
# Insert VERSION v27.0.0
|
||||
cursor.execute("INSERT INTO Settings (setKey, setValue) VALUES ('VERSION', '27.0.0')")
|
||||
conn.commit()
|
||||
|
||||
result = migrate_timestamps_to_utc(cursor)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_migrate_version_26_3_0_skips_migration(self, temp_db):
|
||||
"""Test that v26.3.0 skips migration (newer minor version)"""
|
||||
cursor, conn = temp_db
|
||||
|
||||
# Insert VERSION v26.3.0
|
||||
cursor.execute("INSERT INTO Settings (setKey, setValue) VALUES ('VERSION', '26.3.0')")
|
||||
conn.commit()
|
||||
|
||||
result = migrate_timestamps_to_utc(cursor)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_migrate_old_version_triggers_migration(self, temp_db):
|
||||
"""Test that v25.x.x triggers migration"""
|
||||
cursor, conn = temp_db
|
||||
|
||||
# Insert VERSION v25.1.0
|
||||
cursor.execute("INSERT INTO Settings (setKey, setValue) VALUES ('VERSION', '25.1.0')")
|
||||
|
||||
# Insert a sample device with timestamp
|
||||
now_str = timeNowUTC()
|
||||
cursor.execute("""
|
||||
INSERT INTO Devices (devMac, devFirstConnection, devLastConnection)
|
||||
VALUES ('aa:bb:cc:dd:ee:ff', ?, ?)
|
||||
""", (now_str, now_str))
|
||||
conn.commit()
|
||||
|
||||
result = migrate_timestamps_to_utc(cursor)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_migrate_version_with_v_prefix(self, temp_db):
|
||||
"""Test that version string with 'v' prefix is parsed correctly"""
|
||||
cursor, conn = temp_db
|
||||
|
||||
# Insert VERSION with 'v' prefix
|
||||
cursor.execute("INSERT INTO Settings (setKey, setValue) VALUES ('VERSION', 'v26.2.6')")
|
||||
conn.commit()
|
||||
|
||||
result = migrate_timestamps_to_utc(cursor)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_migrate_malformed_version_uses_fallback(self, temp_db):
|
||||
"""Test that malformed version string uses timestamp detection fallback"""
|
||||
cursor, conn = temp_db
|
||||
|
||||
# Insert malformed VERSION
|
||||
cursor.execute("INSERT INTO Settings (setKey, setValue) VALUES ('VERSION', 'invalid.version')")
|
||||
conn.commit()
|
||||
|
||||
result = migrate_timestamps_to_utc(cursor)
|
||||
|
||||
# Should not crash, should use fallback detection
|
||||
assert result is True
|
||||
|
||||
def test_migrate_version_26_2_5_triggers_migration(self, temp_db):
|
||||
"""Test that v26.2.5 (one patch before UTC) triggers migration"""
|
||||
cursor, conn = temp_db
|
||||
|
||||
# Insert VERSION v26.2.5
|
||||
cursor.execute("INSERT INTO Settings (setKey, setValue) VALUES ('VERSION', '26.2.5')")
|
||||
|
||||
# Insert sample device
|
||||
now_str = timeNowUTC()
|
||||
cursor.execute("""
|
||||
INSERT INTO Devices (devMac, devFirstConnection)
|
||||
VALUES ('aa:bb:cc:dd:ee:ff', ?)
|
||||
""", (now_str,))
|
||||
conn.commit()
|
||||
|
||||
result = migrate_timestamps_to_utc(cursor)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_migrate_does_not_crash_on_empty_devices_table(self, temp_db):
|
||||
"""Test that migration handles empty Devices table gracefully"""
|
||||
cursor, conn = temp_db
|
||||
|
||||
# Insert old VERSION but no devices
|
||||
cursor.execute("INSERT INTO Settings (setKey, setValue) VALUES ('VERSION', '25.1.0')")
|
||||
conn.commit()
|
||||
|
||||
result = migrate_timestamps_to_utc(cursor)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_is_timestamps_in_utc_returns_true_for_empty_table(self, temp_db):
|
||||
"""Test that is_timestamps_in_utc returns True for empty Devices table"""
|
||||
cursor, conn = temp_db
|
||||
|
||||
result = is_timestamps_in_utc(cursor)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_is_timestamps_in_utc_detects_utc_timestamps(self, temp_db):
|
||||
"""Test that is_timestamps_in_utc correctly identifies UTC timestamps"""
|
||||
cursor, conn = temp_db
|
||||
|
||||
# Insert devices with UTC timestamps
|
||||
now_str = timeNowUTC()
|
||||
cursor.execute("""
|
||||
INSERT INTO Devices (devMac, devFirstConnection)
|
||||
VALUES ('aa:bb:cc:dd:ee:ff', ?)
|
||||
""", (now_str,))
|
||||
conn.commit()
|
||||
|
||||
result = is_timestamps_in_utc(cursor)
|
||||
|
||||
# Should return False for naive timestamps (no timezone marker)
|
||||
# This is expected behavior - naive timestamps need migration check
|
||||
assert result is False
|
||||
|
||||
def test_is_timestamps_in_utc_detects_timezone_markers(self, temp_db):
|
||||
"""Test that is_timestamps_in_utc detects timestamps with timezone info"""
|
||||
cursor, conn = temp_db
|
||||
|
||||
# Insert device with timezone marker
|
||||
timestamp_with_tz = "2026-02-11 11:37:02+00:00"
|
||||
cursor.execute("""
|
||||
INSERT INTO Devices (devMac, devFirstConnection)
|
||||
VALUES ('aa:bb:cc:dd:ee:ff', ?)
|
||||
""", (timestamp_with_tz,))
|
||||
conn.commit()
|
||||
|
||||
result = is_timestamps_in_utc(cursor)
|
||||
|
||||
# Should detect timezone marker
|
||||
assert result is True
|
||||
106
test/server/test_datetime_utils.py
Normal file
106
test/server/test_datetime_utils.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
Unit tests for datetime_utils.py UTC timestamp functions.
|
||||
|
||||
Tests verify that:
|
||||
- timeNowUTC() returns correct formats (string and datetime object)
|
||||
- All timestamps are in UTC timezone
|
||||
- No other functions call datetime.datetime.now() (single source of truth)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import datetime
|
||||
import pytest
|
||||
|
||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from utils.datetime_utils import timeNowUTC, DATETIME_PATTERN # noqa: E402
|
||||
|
||||
|
||||
class TestTimeNowUTC:
|
||||
"""Test suite for timeNowUTC() function"""
|
||||
|
||||
def test_timeNowUTC_returns_string_by_default(self):
|
||||
"""Test that timeNowUTC() returns a string by default"""
|
||||
result = timeNowUTC()
|
||||
assert isinstance(result, str)
|
||||
assert len(result) == 19 # 'YYYY-MM-DD HH:MM:SS' format
|
||||
|
||||
def test_timeNowUTC_string_format(self):
|
||||
"""Test that timeNowUTC() returns correct string format"""
|
||||
result = timeNowUTC()
|
||||
# Verify format matches DATETIME_PATTERN
|
||||
try:
|
||||
datetime.datetime.strptime(result, DATETIME_PATTERN)
|
||||
except ValueError:
|
||||
pytest.fail(f"timeNowUTC() returned invalid format: {result}")
|
||||
|
||||
def test_timeNowUTC_returns_datetime_object_when_false(self):
|
||||
"""Test that timeNowUTC(as_string=False) returns datetime object"""
|
||||
result = timeNowUTC(as_string=False)
|
||||
assert isinstance(result, datetime.datetime)
|
||||
|
||||
def test_timeNowUTC_datetime_has_UTC_timezone(self):
|
||||
"""Test that datetime object has UTC timezone"""
|
||||
result = timeNowUTC(as_string=False)
|
||||
assert result.tzinfo is datetime.UTC
|
||||
|
||||
def test_timeNowUTC_datetime_no_microseconds(self):
|
||||
"""Test that datetime object has microseconds set to 0"""
|
||||
result = timeNowUTC(as_string=False)
|
||||
assert result.microsecond == 0
|
||||
|
||||
def test_timeNowUTC_consistency_between_modes(self):
|
||||
"""Test that string and datetime modes return consistent values"""
|
||||
dt_obj = timeNowUTC(as_string=False)
|
||||
str_result = timeNowUTC(as_string=True)
|
||||
|
||||
# Convert datetime to string and compare (within 1 second tolerance)
|
||||
dt_str = dt_obj.strftime(DATETIME_PATTERN)
|
||||
# Parse both to compare timestamps
|
||||
t1 = datetime.datetime.strptime(dt_str, DATETIME_PATTERN)
|
||||
t2 = datetime.datetime.strptime(str_result, DATETIME_PATTERN)
|
||||
diff = abs((t1 - t2).total_seconds())
|
||||
assert diff <= 1 # Allow 1 second difference
|
||||
|
||||
def test_timeNowUTC_is_actually_UTC(self):
|
||||
"""Test that timeNowUTC() returns actual UTC time, not local time"""
|
||||
utc_now = datetime.datetime.now(datetime.UTC).replace(microsecond=0)
|
||||
result = timeNowUTC(as_string=False)
|
||||
|
||||
# Should be within 1 second
|
||||
diff = abs((utc_now - result).total_seconds())
|
||||
assert diff <= 1
|
||||
|
||||
def test_timeNowUTC_string_matches_datetime_conversion(self):
|
||||
"""Test that string result matches datetime object conversion"""
|
||||
dt_obj = timeNowUTC(as_string=False)
|
||||
str_result = timeNowUTC(as_string=True)
|
||||
|
||||
# Convert datetime to string using same format
|
||||
expected = dt_obj.strftime(DATETIME_PATTERN)
|
||||
|
||||
# Should be same or within 1 second
|
||||
t1 = datetime.datetime.strptime(expected, DATETIME_PATTERN)
|
||||
t2 = datetime.datetime.strptime(str_result, DATETIME_PATTERN)
|
||||
diff = abs((t1 - t2).total_seconds())
|
||||
assert diff <= 1
|
||||
|
||||
def test_timeNowUTC_explicit_true_parameter(self):
|
||||
"""Test that timeNowUTC(as_string=True) explicitly returns string"""
|
||||
result = timeNowUTC(as_string=True)
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_timeNowUTC_multiple_calls_increase(self):
|
||||
"""Test that subsequent calls return increasing timestamps"""
|
||||
import time
|
||||
|
||||
t1_str = timeNowUTC()
|
||||
time.sleep(0.1)
|
||||
t2_str = timeNowUTC()
|
||||
|
||||
t1 = datetime.datetime.strptime(t1_str, DATETIME_PATTERN)
|
||||
t2 = datetime.datetime.strptime(t2_str, DATETIME_PATTERN)
|
||||
|
||||
assert t2 >= t1
|
||||
Reference in New Issue
Block a user