mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-31 07:12:23 -07:00
267 lines
10 KiB
Python
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()
|