mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-25 02:43:23 -04:00
Implement notification text templates and update related settings for customizable notifications
This commit is contained in:
2
.github/skills/code-standards/SKILL.md
vendored
2
.github/skills/code-standards/SKILL.md
vendored
@@ -64,7 +64,7 @@ Use timeNowUTC(as_string=False) for datetime operations (scheduling, comparisons
|
||||
|
||||
## String Sanitization
|
||||
|
||||
Use sanitizers from `server/helper.py` before storing user input.
|
||||
Use sanitizers from `server/helper.py` before storing user input. MAC addresses are always lowercased and normalized. IP addresses should be validated.
|
||||
|
||||
## Devcontainer Constraints
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Notifications 📧
|
||||
|
||||
> [!TIP]
|
||||
> Want to customize how devices appear in text notifications? See [Notification Text Templates](NOTIFICATION_TEMPLATES.md).
|
||||
|
||||
There are 4 ways how to influence notifications:
|
||||
|
||||
1. On the device itself
|
||||
|
||||
110
docs/NOTIFICATION_TEMPLATES.md
Normal file
110
docs/NOTIFICATION_TEMPLATES.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Notification Text Templates
|
||||
|
||||
> Customize how devices and events appear in **text** notifications (email previews, push notifications, Apprise messages).
|
||||
|
||||
By default, NetAlertX formats each device as a vertical list of `Header: Value` pairs. Text templates let you define a **single-line format per device** using `{FieldName}` placeholders — ideal for mobile notification previews and high-volume alerts.
|
||||
|
||||
HTML email tables are **not affected** by these templates.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Go to **Settings → Notification Processing**.
|
||||
2. Set a template string for the section you want to customize, e.g.:
|
||||
- **Text Template: New Devices** → `{Device name} ({MAC}) - {IP}`
|
||||
3. Save. The next notification will use your format.
|
||||
|
||||
**Before (default):**
|
||||
```
|
||||
🆕 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:
|
||||
```
|
||||
|
||||
**After (with template `{Device name} ({MAC}) - {IP}`):**
|
||||
```
|
||||
🆕 New devices
|
||||
---------
|
||||
MyPhone (aa:bb:cc:dd:ee:ff) - 192.168.1.42
|
||||
```
|
||||
|
||||
## Settings Reference
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `NTFPRCS_TEXT_SECTION_HEADERS` | Boolean | `true` | Show/hide section titles (e.g. `🆕 New devices \n---------`). |
|
||||
| `NTFPRCS_TEXT_TEMPLATE_new_devices` | String | *(empty)* | Template for new device rows. |
|
||||
| `NTFPRCS_TEXT_TEMPLATE_down_devices` | String | *(empty)* | Template for down device rows. |
|
||||
| `NTFPRCS_TEXT_TEMPLATE_down_reconnected` | String | *(empty)* | Template for reconnected device rows. |
|
||||
| `NTFPRCS_TEXT_TEMPLATE_events` | String | *(empty)* | Template for event rows. |
|
||||
| `NTFPRCS_TEXT_TEMPLATE_plugins` | String | *(empty)* | Template for plugin event rows. |
|
||||
|
||||
When a template is **empty**, the section uses the original vertical `Header: Value` format (full backward compatibility).
|
||||
|
||||
## Template Syntax
|
||||
|
||||
Use `{FieldName}` to insert a value from the notification data. Field names are **case-sensitive** and must match the column names exactly.
|
||||
|
||||
```
|
||||
{Device name} ({MAC}) connected at {Datetime}
|
||||
```
|
||||
|
||||
- No loops, conditionals, or nesting — just simple string replacement.
|
||||
- If a `{FieldName}` does not exist in the data, it is left as-is in the output (safe failure). For example, `{NonExistent}` renders literally as `{NonExistent}`.
|
||||
|
||||
## Variable Availability by Section
|
||||
|
||||
Each section has different available fields because they come from different database queries.
|
||||
|
||||
### `new_devices` and `events`
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `{MAC}` | Device MAC address |
|
||||
| `{Datetime}` | Event timestamp |
|
||||
| `{IP}` | Device IP address |
|
||||
| `{Event Type}` | Type of event (e.g. `New Device`, `Connected`) |
|
||||
| `{Device name}` | Device display name |
|
||||
| `{Comments}` | Device comments |
|
||||
|
||||
**Example:** `{Device name} ({MAC}) - {IP} [{Event Type}]`
|
||||
|
||||
### `down_devices` and `down_reconnected`
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `{devName}` | Device display name |
|
||||
| `{eve_MAC}` | Device MAC address |
|
||||
| `{devVendor}` | Device vendor/manufacturer |
|
||||
| `{eve_IP}` | Device IP address |
|
||||
| `{eve_DateTime}` | Event timestamp |
|
||||
| `{eve_EventType}` | Type of event |
|
||||
|
||||
**Example:** `{devName} ({eve_MAC}) {devVendor} - went down at {eve_DateTime}`
|
||||
|
||||
### `plugins`
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `{Plugin}` | Plugin code name |
|
||||
| `{Object_PrimaryId}` | Primary identifier of the object |
|
||||
| `{Object_SecondaryId}` | Secondary identifier |
|
||||
| `{DateTimeChanged}` | Timestamp of change |
|
||||
| `{Watched_Value1}` | First watched value |
|
||||
| `{Watched_Value2}` | Second watched value |
|
||||
| `{Watched_Value3}` | Third watched value |
|
||||
| `{Watched_Value4}` | Fourth watched value |
|
||||
| `{Status}` | Plugin event status |
|
||||
|
||||
**Example:** `{Plugin}: {Object_PrimaryId} - {Status}`
|
||||
|
||||
> [!NOTE]
|
||||
> Field names differ between sections because they come from different SQL queries. A template configured for `new_devices` cannot use `{devName}` — that field is only available in `down_devices` and `down_reconnected`.
|
||||
|
||||
## Section Headers Toggle
|
||||
|
||||
Set **Text Section Headers** (`NTFPRCS_TEXT_SECTION_HEADERS`) to `false` to remove the section title separators from text notifications. This is useful when you want compact output without the `🆕 New devices \n---------` banners.
|
||||
@@ -180,6 +180,150 @@
|
||||
"string": "You can specify a SQL where condition to filter out Events from notifications. For example <code>AND devLastIP NOT LIKE '192.168.3.%'</code> will always exclude any Event notifications for all devices with the IP starting with <code>192.168.3.%</code>."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "TEXT_SECTION_HEADERS",
|
||||
"type": {
|
||||
"dataType": "boolean",
|
||||
"elements": [
|
||||
{ "elementType": "input", "elementOptions": [{ "type": "checkbox" }], "transformers": [] }
|
||||
]
|
||||
},
|
||||
"default_value": true,
|
||||
"options": [],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Text Section Headers"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Enable or disable section titles (e.g. <code>🆕 New devices \\n---------</code>) in text notifications. Enabled by default for backward compatibility."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "TEXT_TEMPLATE_new_devices",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{ "elementType": "input", "elementOptions": [], "transformers": [] }
|
||||
]
|
||||
},
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Text Template: New Devices"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Custom text template for new device notifications. Use <code>{FieldName}</code> placeholders, e.g. <code>{Device name} ({MAC}) - {IP}</code>. Leave empty for default formatting. Available fields: <code>{MAC}</code>, <code>{Datetime}</code>, <code>{IP}</code>, <code>{Event Type}</code>, <code>{Device name}</code>, <code>{Comments}</code>."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "TEXT_TEMPLATE_down_devices",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{ "elementType": "input", "elementOptions": [], "transformers": [] }
|
||||
]
|
||||
},
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Text Template: Down Devices"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Custom text template for down device notifications. Use <code>{FieldName}</code> placeholders, e.g. <code>{devName} ({eve_MAC}) - {eve_IP}</code>. Leave empty for default formatting. Available fields: <code>{devName}</code>, <code>{eve_MAC}</code>, <code>{devVendor}</code>, <code>{eve_IP}</code>, <code>{eve_DateTime}</code>, <code>{eve_EventType}</code>."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "TEXT_TEMPLATE_down_reconnected",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{ "elementType": "input", "elementOptions": [], "transformers": [] }
|
||||
]
|
||||
},
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Text Template: Reconnected"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Custom text template for reconnected device notifications. Use <code>{FieldName}</code> placeholders. Leave empty for default formatting. Available fields: <code>{devName}</code>, <code>{eve_MAC}</code>, <code>{devVendor}</code>, <code>{eve_IP}</code>, <code>{eve_DateTime}</code>, <code>{eve_EventType}</code>."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "TEXT_TEMPLATE_events",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{ "elementType": "input", "elementOptions": [], "transformers": [] }
|
||||
]
|
||||
},
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Text Template: Events"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Custom text template for event notifications. Use <code>{FieldName}</code> placeholders, e.g. <code>{Device name} ({MAC}) {Event Type} at {Datetime}</code>. Leave empty for default formatting. Available fields: <code>{MAC}</code>, <code>{Datetime}</code>, <code>{IP}</code>, <code>{Event Type}</code>, <code>{Device name}</code>, <code>{Comments}</code>."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "TEXT_TEMPLATE_plugins",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{ "elementType": "input", "elementOptions": [], "transformers": [] }
|
||||
]
|
||||
},
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Text Template: Plugins"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Custom text template for plugin event notifications. Use <code>{FieldName}</code> placeholders, e.g. <code>{Plugin}: {Object_PrimaryId} - {Status}</code>. Leave empty for default formatting. Available fields: <code>{Plugin}</code>, <code>{Object_PrimaryId}</code>, <code>{Object_SecondaryId}</code>, <code>{DateTimeChanged}</code>, <code>{Watched_Value1}</code>, <code>{Watched_Value2}</code>, <code>{Watched_Value3}</code>, <code>{Watched_Value4}</code>, <code>{Status}</code>."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ nav:
|
||||
- Advanced guides:
|
||||
- Remote Networks: REMOTE_NETWORKS.md
|
||||
- Notifications Guide: NOTIFICATIONS.md
|
||||
- Notification Text Templates: NOTIFICATION_TEMPLATES.md
|
||||
- Custom PUID/GUID: PUID_PGID_SECURITY.md
|
||||
- Name Resolution: NAME_RESOLUTION.md
|
||||
- Authelia: AUTHELIA.md
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
import socket
|
||||
from yattag import indent
|
||||
@@ -345,8 +346,16 @@ def construct_notifications(JSON, section):
|
||||
build_direction = "TOP_TO_BOTTOM"
|
||||
text_line = "{}\t{}\n"
|
||||
|
||||
# Read template settings
|
||||
show_headers = get_setting_value("NTFPRCS_TEXT_SECTION_HEADERS")
|
||||
if show_headers is None or show_headers == "":
|
||||
show_headers = True
|
||||
text_template = get_setting_value(f"NTFPRCS_TEXT_TEMPLATE_{section}") or ""
|
||||
|
||||
if len(jsn) > 0:
|
||||
text = tableTitle + "\n---------\n"
|
||||
# Section header (text)
|
||||
if show_headers:
|
||||
text = tableTitle + "\n---------\n"
|
||||
|
||||
# Convert a JSON into an HTML table
|
||||
html = convert(
|
||||
@@ -363,13 +372,24 @@ def construct_notifications(JSON, section):
|
||||
)
|
||||
|
||||
# prepare text-only message
|
||||
for device in jsn:
|
||||
for header in headers:
|
||||
padding = ""
|
||||
if len(header) < 4:
|
||||
padding = "\t"
|
||||
text += text_line.format(header + ": " + padding, device[header])
|
||||
text += "\n"
|
||||
if text_template:
|
||||
# Custom template: replace {FieldName} placeholders per device
|
||||
for device in jsn:
|
||||
line = re.sub(
|
||||
r'\{(.+?)\}',
|
||||
lambda m: str(device.get(m.group(1), m.group(0))),
|
||||
text_template,
|
||||
)
|
||||
text += line + "\n"
|
||||
else:
|
||||
# Legacy fallback: vertical Header: Value list
|
||||
for device in jsn:
|
||||
for header in headers:
|
||||
padding = ""
|
||||
if len(header) < 4:
|
||||
padding = "\t"
|
||||
text += text_line.format(header + ": " + padding, device[header])
|
||||
text += "\n"
|
||||
|
||||
# Format HTML table headers
|
||||
for header in headers:
|
||||
|
||||
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