Files
NetAlertX/test/backend/test_notification_templates.py

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()