Files
NetAlertX/test/backend/test_notification_templates.py

267 lines
10 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 = [
{
"MAC": "AA:BB:CC:DD:EE:FF",
"Datetime": "2025-01-15 10:30:00",
"IP": "192.168.1.42",
"Event Type": "New Device",
"Device name": "MyPhone",
"Comments": "",
},
{
"MAC": "11:22:33:44:55:66",
"Datetime": "2025-01-15 11:00:00",
"IP": "192.168.1.99",
"Event Type": "New Device",
"Device name": "Laptop",
"Comments": "Office",
},
]
NEW_DEVICE_COLUMNS = ["MAC", "Datetime", "IP", "Event Type", "Device name", "Comments"]
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("MAC:", text)
self.assertIn("AA:BB:CC:DD:EE:FF", text)
self.assertIn("Device name:", 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": "{Device name} ({MAC}) - {IP}",
})
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": "{Device name} - {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": "{Device name} ({MAC})",
})
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": "{Device name} ({BadField}) - {IP}",
})
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 different column names
# -----------------------------------------------------------------
@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} ({eve_MAC}) down since {eve_DateTime}",
})
down_devices = [
{
"devName": "Router",
"eve_MAC": "FF:EE:DD:CC:BB:AA",
"devVendor": "Cisco",
"eve_IP": "10.0.0.1",
"eve_DateTime": "2025-01-15 08:00:00",
"eve_EventType": "Device Down",
}
]
columns = ["devName", "eve_MAC", "devVendor", "eve_IP", "eve_DateTime", "eve_EventType"]
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)
# -----------------------------------------------------------------
# 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": "{Device name} ({MAC})",
})
html_with, _ = construct_notifications(json_data, "new_devices")
self.assertEqual(html_without, html_with)
if __name__ == "__main__":
unittest.main()