Files
NetAlertX/test/server/test_plugin_identity_collision.py
T

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"