mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-01 07:42:19 -07:00
Implement notification text templates and update related settings for customizable notifications
This commit is contained in:
266
test/backend/test_notification_templates.py
Normal file
266
test/backend/test_notification_templates.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user