mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-26 03:13:14 -04:00
300 lines
12 KiB
Python
300 lines
12 KiB
Python
"""
|
|
NetAlertX Notification Text Template Tests
|
|
|
|
Tests the template substitution and section header toggle in
|
|
construct_notifications(). All tests mock get_setting_value to avoid
|
|
database/config dependencies.
|
|
|
|
License: GNU GPLv3
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
# Add the server directory to the path for imports
|
|
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
|
sys.path.extend([f"{INSTALL_PATH}/server"])
|
|
|
|
|
|
def _make_json(section, devices, column_names, title="Test Section"):
|
|
"""Helper to build the JSON structure expected by construct_notifications."""
|
|
return {
|
|
section: devices,
|
|
f"{section}_meta": {
|
|
"title": title,
|
|
"columnNames": column_names,
|
|
},
|
|
}
|
|
|
|
|
|
SAMPLE_NEW_DEVICES = [
|
|
{
|
|
"devName": "MyPhone",
|
|
"eveMac": "aa:bb:cc:dd:ee:ff",
|
|
"devVendor": "",
|
|
"eveIp": "192.168.1.42",
|
|
"eveDateTime": "2025-01-15 10:30:00",
|
|
"eveEventType": "New Device",
|
|
"devComments": "",
|
|
},
|
|
{
|
|
"devName": "Laptop",
|
|
"eveMac": "11:22:33:44:55:66",
|
|
"devVendor": "Dell",
|
|
"eveIp": "192.168.1.99",
|
|
"eveDateTime": "2025-01-15 11:00:00",
|
|
"eveEventType": "New Device",
|
|
"devComments": "Office",
|
|
},
|
|
]
|
|
|
|
NEW_DEVICE_COLUMNS = ["devName", "eveMac", "devVendor", "eveIp", "eveDateTime", "eveEventType", "devComments"]
|
|
|
|
|
|
class TestConstructNotificationsTemplates(unittest.TestCase):
|
|
"""Tests for template substitution in construct_notifications."""
|
|
|
|
def _setting_factory(self, overrides=None):
|
|
"""Return a mock get_setting_value that resolves from overrides dict."""
|
|
settings = overrides or {}
|
|
|
|
def mock_get(key):
|
|
return settings.get(key, "")
|
|
|
|
return mock_get
|
|
|
|
# -----------------------------------------------------------------
|
|
# Empty section should always return ("", "") regardless of settings
|
|
# -----------------------------------------------------------------
|
|
@patch("models.notification_instance.get_setting_value")
|
|
def test_empty_section_returns_empty(self, mock_setting):
|
|
from models.notification_instance import construct_notifications
|
|
|
|
mock_setting.return_value = ""
|
|
json_data = _make_json("new_devices", [], [])
|
|
html, text = construct_notifications(json_data, "new_devices")
|
|
self.assertEqual(html, "")
|
|
self.assertEqual(text, "")
|
|
|
|
# -----------------------------------------------------------------
|
|
# Legacy fallback: no template → vertical Header: Value per device
|
|
# -----------------------------------------------------------------
|
|
@patch("models.notification_instance.get_setting_value")
|
|
def test_legacy_fallback_no_template(self, mock_setting):
|
|
from models.notification_instance import construct_notifications
|
|
|
|
mock_setting.side_effect = self._setting_factory({
|
|
"NTFPRCS_TEXT_SECTION_HEADERS": True,
|
|
"NTFPRCS_TEXT_TEMPLATE_new_devices": "",
|
|
})
|
|
|
|
json_data = _make_json(
|
|
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
|
|
)
|
|
html, text = construct_notifications(json_data, "new_devices")
|
|
|
|
# Section header must be present
|
|
self.assertIn("🆕 New devices", text)
|
|
self.assertIn("---------", text)
|
|
|
|
# Legacy format: each header appears as "Header: \tValue"
|
|
self.assertIn("eveMac:", text)
|
|
self.assertIn("aa:bb:cc:dd:ee:ff", text)
|
|
self.assertIn("devName:", text)
|
|
self.assertIn("MyPhone", text)
|
|
|
|
# HTML must still be generated
|
|
self.assertNotEqual(html, "")
|
|
|
|
# -----------------------------------------------------------------
|
|
# Template substitution: single-line format per device
|
|
# -----------------------------------------------------------------
|
|
@patch("models.notification_instance.get_setting_value")
|
|
def test_template_substitution(self, mock_setting):
|
|
from models.notification_instance import construct_notifications
|
|
|
|
mock_setting.side_effect = self._setting_factory({
|
|
"NTFPRCS_TEXT_SECTION_HEADERS": True,
|
|
"NTFPRCS_TEXT_TEMPLATE_new_devices": "{devName} ({eveMac}) - {eveIp}",
|
|
})
|
|
|
|
json_data = _make_json(
|
|
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
|
|
)
|
|
_, text = construct_notifications(json_data, "new_devices")
|
|
|
|
self.assertIn("MyPhone (aa:bb:cc:dd:ee:ff) - 192.168.1.42", text)
|
|
self.assertIn("Laptop (11:22:33:44:55:66) - 192.168.1.99", text)
|
|
|
|
# -----------------------------------------------------------------
|
|
# Missing field: {NonExistent} left as-is (safe failure)
|
|
# -----------------------------------------------------------------
|
|
@patch("models.notification_instance.get_setting_value")
|
|
def test_missing_field_safe_failure(self, mock_setting):
|
|
from models.notification_instance import construct_notifications
|
|
|
|
mock_setting.side_effect = self._setting_factory({
|
|
"NTFPRCS_TEXT_SECTION_HEADERS": True,
|
|
"NTFPRCS_TEXT_TEMPLATE_new_devices": "{devName} - {NonExistent}",
|
|
})
|
|
|
|
json_data = _make_json(
|
|
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
|
|
)
|
|
_, text = construct_notifications(json_data, "new_devices")
|
|
|
|
self.assertIn("MyPhone - {NonExistent}", text)
|
|
self.assertIn("Laptop - {NonExistent}", text)
|
|
|
|
# -----------------------------------------------------------------
|
|
# Section headers disabled: no title/separator in text output
|
|
# -----------------------------------------------------------------
|
|
@patch("models.notification_instance.get_setting_value")
|
|
def test_section_headers_disabled(self, mock_setting):
|
|
from models.notification_instance import construct_notifications
|
|
|
|
mock_setting.side_effect = self._setting_factory({
|
|
"NTFPRCS_TEXT_SECTION_HEADERS": False,
|
|
"NTFPRCS_TEXT_TEMPLATE_new_devices": "{devName} ({eveMac})",
|
|
})
|
|
|
|
json_data = _make_json(
|
|
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
|
|
)
|
|
_, text = construct_notifications(json_data, "new_devices")
|
|
|
|
self.assertNotIn("🆕 New devices", text)
|
|
self.assertNotIn("---------", text)
|
|
# Template output still present
|
|
self.assertIn("MyPhone (aa:bb:cc:dd:ee:ff)", text)
|
|
|
|
# -----------------------------------------------------------------
|
|
# Section headers enabled (default when setting absent/empty)
|
|
# -----------------------------------------------------------------
|
|
@patch("models.notification_instance.get_setting_value")
|
|
def test_section_headers_default_enabled(self, mock_setting):
|
|
from models.notification_instance import construct_notifications
|
|
|
|
# Simulate setting not configured (returns empty string)
|
|
mock_setting.side_effect = self._setting_factory({})
|
|
|
|
json_data = _make_json(
|
|
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
|
|
)
|
|
_, text = construct_notifications(json_data, "new_devices")
|
|
|
|
# Headers should be shown by default
|
|
self.assertIn("🆕 New devices", text)
|
|
self.assertIn("---------", text)
|
|
|
|
# -----------------------------------------------------------------
|
|
# Mixed valid and invalid fields in same template
|
|
# -----------------------------------------------------------------
|
|
@patch("models.notification_instance.get_setting_value")
|
|
def test_mixed_valid_and_invalid_fields(self, mock_setting):
|
|
from models.notification_instance import construct_notifications
|
|
|
|
mock_setting.side_effect = self._setting_factory({
|
|
"NTFPRCS_TEXT_SECTION_HEADERS": True,
|
|
"NTFPRCS_TEXT_TEMPLATE_new_devices": "{devName} ({BadField}) - {eveIp}",
|
|
})
|
|
|
|
json_data = _make_json(
|
|
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
|
|
)
|
|
_, text = construct_notifications(json_data, "new_devices")
|
|
|
|
self.assertIn("MyPhone ({BadField}) - 192.168.1.42", text)
|
|
|
|
# -----------------------------------------------------------------
|
|
# Down devices section uses same column names as all other sections
|
|
# -----------------------------------------------------------------
|
|
@patch("models.notification_instance.get_setting_value")
|
|
def test_down_devices_template(self, mock_setting):
|
|
from models.notification_instance import construct_notifications
|
|
|
|
mock_setting.side_effect = self._setting_factory({
|
|
"NTFPRCS_TEXT_SECTION_HEADERS": True,
|
|
"NTFPRCS_TEXT_TEMPLATE_down_devices": "{devName} ({eveMac}) down since {eveDateTime}",
|
|
})
|
|
|
|
down_devices = [
|
|
{
|
|
"devName": "Router",
|
|
"eveMac": "ff:ee:dd:cc:bb:aa",
|
|
"devVendor": "Cisco",
|
|
"eveIp": "10.0.0.1",
|
|
"eveDateTime": "2025-01-15 08:00:00",
|
|
"eveEventType": "Device Down",
|
|
"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")
|
|
|
|
self.assertIn("Router (ff:ee:dd:cc:bb:aa) down since 2025-01-15 08:00:00", text)
|
|
|
|
# -----------------------------------------------------------------
|
|
# Down reconnected section uses same unified column names
|
|
# -----------------------------------------------------------------
|
|
@patch("models.notification_instance.get_setting_value")
|
|
def test_down_reconnected_template(self, mock_setting):
|
|
from models.notification_instance import construct_notifications
|
|
|
|
mock_setting.side_effect = self._setting_factory({
|
|
"NTFPRCS_TEXT_SECTION_HEADERS": True,
|
|
"NTFPRCS_TEXT_TEMPLATE_down_reconnected": "{devName} ({eveMac}) reconnected at {eveDateTime}",
|
|
})
|
|
|
|
reconnected = [
|
|
{
|
|
"devName": "Switch",
|
|
"eveMac": "aa:11:bb:22:cc:33",
|
|
"devVendor": "Netgear",
|
|
"eveIp": "10.0.0.2",
|
|
"eveDateTime": "2025-01-15 09:30:00",
|
|
"eveEventType": "Down Reconnected",
|
|
"devComments": "",
|
|
}
|
|
]
|
|
columns = ["devName", "eveMac", "devVendor", "eveIp", "eveDateTime", "eveEventType", "devComments"]
|
|
|
|
json_data = _make_json("down_reconnected", reconnected, columns, "🔁 Reconnected down devices")
|
|
_, text = construct_notifications(json_data, "down_reconnected")
|
|
|
|
self.assertIn("Switch (aa:11:bb:22:cc:33) reconnected at 2025-01-15 09:30:00", text)
|
|
|
|
# -----------------------------------------------------------------
|
|
# HTML output is unchanged regardless of template config
|
|
# -----------------------------------------------------------------
|
|
@patch("models.notification_instance.get_setting_value")
|
|
def test_html_unchanged_with_template(self, mock_setting):
|
|
from models.notification_instance import construct_notifications
|
|
|
|
# Get HTML without template
|
|
mock_setting.side_effect = self._setting_factory({
|
|
"NTFPRCS_TEXT_SECTION_HEADERS": True,
|
|
"NTFPRCS_TEXT_TEMPLATE_new_devices": "",
|
|
})
|
|
json_data = _make_json(
|
|
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
|
|
)
|
|
html_without, _ = construct_notifications(json_data, "new_devices")
|
|
|
|
# Get HTML with template
|
|
mock_setting.side_effect = self._setting_factory({
|
|
"NTFPRCS_TEXT_SECTION_HEADERS": True,
|
|
"NTFPRCS_TEXT_TEMPLATE_new_devices": "{devName} ({eveMac})",
|
|
})
|
|
html_with, _ = construct_notifications(json_data, "new_devices")
|
|
|
|
self.assertEqual(html_without, html_with)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|