mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-10-02 10:45:05 -04:00
170 lines
6.1 KiB
Python
170 lines
6.1 KiB
Python
"""
|
|
Tests for the identity-hash collision guard in process_plugin_events().
|
|
|
|
If two events in the same plugin run resolve to the same idsHash (same
|
|
sanitized primaryId + secondaryId), nothing in the merge loop expects that -
|
|
merging both into the same Plugins_Objects row would silently conflate two
|
|
distinct discovered identities. process_plugin_events() now rejects the
|
|
whole run's batch (persists nothing) when this happens, without picking a
|
|
winner, and leaves a run with no collision unaffected.
|
|
|
|
Run from inside the NetAlertX container - server/plugin.py isn't importable
|
|
standalone outside it (real conf/database/api imports).
|
|
|
|
pytest "test/server/test_plugin_identity_collision.py" -v
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Path setup
|
|
# ---------------------------------------------------------------------------
|
|
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
|
sys.path.extend([f"{INSTALL_PATH}/server/plugins", f"{INSTALL_PATH}/server"])
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
from db_test_helpers import ( # noqa: E402
|
|
make_plugin_db,
|
|
make_plugin_dict,
|
|
make_plugin_event_row,
|
|
seed_plugin_object,
|
|
plugin_objects_rows,
|
|
)
|
|
|
|
import plugin as plugin_module # noqa: E402
|
|
from plugin import process_plugin_events # noqa: E402
|
|
|
|
PREFIX = "TESTPLG"
|
|
|
|
|
|
@pytest.fixture
|
|
def plugin_db():
|
|
"""Yield a (PluginFakeDB, connection) backed by an in-memory SQLite database."""
|
|
db, conn = make_plugin_db()
|
|
yield db, conn
|
|
conn.close()
|
|
|
|
|
|
def _no_report_on(key, default=""):
|
|
"""Monkeypatch target: return empty REPORT_ON so no events are generated."""
|
|
if key.endswith("_REPORT_ON"):
|
|
return []
|
|
return default
|
|
|
|
|
|
class TestCollisionRejectsWholeBatch:
|
|
def test_colliding_events_persist_nothing(self, plugin_db, monkeypatch):
|
|
db, conn = plugin_db
|
|
monkeypatch.setattr("plugin.get_setting_value", _no_report_on)
|
|
|
|
plugin = make_plugin_dict(PREFIX)
|
|
events = [
|
|
make_plugin_event_row(PREFIX, "device_A", secondary_id="sec"),
|
|
make_plugin_event_row(PREFIX, "device_A", secondary_id="sec", watched1="different"),
|
|
]
|
|
|
|
process_plugin_events(db, plugin, events)
|
|
|
|
assert plugin_objects_rows(conn, PREFIX) == []
|
|
|
|
def test_collision_does_not_touch_preexisting_objects(self, plugin_db, monkeypatch):
|
|
"""A rejected batch must not modify unrelated, already-persisted
|
|
objects from prior runs either."""
|
|
db, conn = plugin_db
|
|
monkeypatch.setattr("plugin.get_setting_value", _no_report_on)
|
|
|
|
cur = conn.cursor()
|
|
seed_plugin_object(cur, PREFIX, "existing_device", watched1="val1",
|
|
status="watched-not-changed")
|
|
conn.commit()
|
|
|
|
plugin = make_plugin_dict(PREFIX)
|
|
events = [
|
|
make_plugin_event_row(PREFIX, "device_A", secondary_id="sec"),
|
|
make_plugin_event_row(PREFIX, "device_A", secondary_id="sec"),
|
|
]
|
|
|
|
process_plugin_events(db, plugin, events)
|
|
|
|
rows = plugin_objects_rows(conn, PREFIX)
|
|
assert len(rows) == 1
|
|
assert rows[0][2] == "existing_device"
|
|
|
|
def test_collision_logged_via_mylog_none(self, plugin_db, monkeypatch):
|
|
db, conn = plugin_db
|
|
monkeypatch.setattr("plugin.get_setting_value", _no_report_on)
|
|
calls = []
|
|
monkeypatch.setattr(plugin_module, "mylog", lambda level, msg: calls.append((level, msg)))
|
|
|
|
plugin = make_plugin_dict(PREFIX)
|
|
events = [
|
|
make_plugin_event_row(PREFIX, "device_A", secondary_id="sec"),
|
|
make_plugin_event_row(PREFIX, "device_A", secondary_id="sec"),
|
|
]
|
|
|
|
process_plugin_events(db, plugin, events)
|
|
|
|
assert any(level == "none" and "collision" in msg for level, msg in calls)
|
|
|
|
|
|
class TestNoCollisionBehavesAsBefore:
|
|
def test_distinct_events_persist_normally(self, plugin_db, monkeypatch):
|
|
db, conn = plugin_db
|
|
monkeypatch.setattr("plugin.get_setting_value", _no_report_on)
|
|
|
|
plugin = make_plugin_dict(PREFIX)
|
|
events = [
|
|
make_plugin_event_row(PREFIX, "device_A"),
|
|
make_plugin_event_row(PREFIX, "device_B"),
|
|
]
|
|
|
|
process_plugin_events(db, plugin, events)
|
|
|
|
rows = plugin_objects_rows(conn, PREFIX)
|
|
ids = {r[2] for r in rows}
|
|
assert ids == {"device_A", "device_B"}
|
|
|
|
def test_concatenation_boundary_shift_is_not_a_false_collision(self, plugin_db, monkeypatch):
|
|
"""idsHash hashes the (primaryId, secondaryId) pair, not their string
|
|
concatenation - ("ab", "c") and ("a", "bc") both concatenate to "abc",
|
|
which would wrongly hash identically and trip the collision guard,
|
|
rejecting a legitimate batch that has no real duplicate in it."""
|
|
db, conn = plugin_db
|
|
monkeypatch.setattr("plugin.get_setting_value", _no_report_on)
|
|
|
|
plugin = make_plugin_dict(PREFIX)
|
|
events = [
|
|
make_plugin_event_row(PREFIX, "ab", secondary_id="c"),
|
|
make_plugin_event_row(PREFIX, "a", secondary_id="bc"),
|
|
]
|
|
|
|
process_plugin_events(db, plugin, events)
|
|
|
|
rows = plugin_objects_rows(conn, PREFIX)
|
|
pairs = {(r[2], r[3]) for r in rows} # (objectPrimaryId, objectSecondaryId)
|
|
assert pairs == {("ab", "c"), ("a", "bc")}
|
|
|
|
def test_event_matching_a_preexisting_object_is_not_a_collision(self, plugin_db, monkeypatch):
|
|
"""The collision check only compares events against each other, not
|
|
against pre-existing Plugins_Objects rows - matching an existing
|
|
object is the normal "exists" path, not a collision."""
|
|
db, conn = plugin_db
|
|
monkeypatch.setattr("plugin.get_setting_value", _no_report_on)
|
|
|
|
cur = conn.cursor()
|
|
seed_plugin_object(cur, PREFIX, "device_A", watched1="val1",
|
|
status="watched-not-changed")
|
|
conn.commit()
|
|
|
|
plugin = make_plugin_dict(PREFIX)
|
|
events = [make_plugin_event_row(PREFIX, "device_A", watched1="val1")]
|
|
|
|
process_plugin_events(db, plugin, events)
|
|
|
|
rows = plugin_objects_rows(conn, PREFIX)
|
|
assert len(rows) == 1
|
|
assert rows[0][2] == "device_A"
|