From aa6098475454e68a159ddc2ba2fda68f141697bb Mon Sep 17 00:00:00 2001 From: Ehsan Date: Mon, 13 Jul 2026 10:26:54 +0300 Subject: [PATCH] fix: safe-empty restock default so restock notification tokens work for any watch (#4249) --- changedetectionio/notification_service.py | 4 ++ .../tests/test_notification_restock_token.py | 43 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 changedetectionio/tests/test_notification_restock_token.py diff --git a/changedetectionio/notification_service.py b/changedetectionio/notification_service.py index 0813d6c81..81669adb5 100644 --- a/changedetectionio/notification_service.py +++ b/changedetectionio/notification_service.py @@ -207,6 +207,10 @@ class NotificationContextData(dict): # Always the raw +/- diff regardless of LLM summary override (populated in handler.py from {{diff}}) 'raw_diff': FormattableDiff('', ''), 'markup_text_links_to_html_links': False, # If automatic conversion of plaintext to HTML should happen + # Safe-empty default so restock tokens ({{ restock.price }} etc.) are a valid, + # non-crashing token for every watch. Restock watches override this via + # processors/restock_diff extra_notification_token_values(). + 'restock': {}, #@TODO ! should be fixed in a refactor of all processor types, maybe fetching a empty key returns null or even restock (or similar) should actually come from the JSON in the datadir processor-restock-diff.json etc 'notification_timestamp': time.time(), 'prev_snapshot': None, 'preview_url': None, diff --git a/changedetectionio/tests/test_notification_restock_token.py b/changedetectionio/tests/test_notification_restock_token.py new file mode 100644 index 000000000..9d994b3df --- /dev/null +++ b/changedetectionio/tests/test_notification_restock_token.py @@ -0,0 +1,43 @@ +""" +Regression tests for issue #3490 - "'restock' is undefined". + +A `{{ restock.price }}` token is accepted in a per-watch notification body (restock +watches inject the value via processors/restock_diff extra_notification_token_values()), +but the same token in a system-wide / non-restock context had no default. That made it: + + 1. crash rendering at send time for a non-restock watch (UndefinedError), and + 2. fail save-time validation of a system-wide notification body (ValidationError), + +because `restock` was absent from NotificationContextData's default token set. + +These tests exercise the real send-time (jinja2_custom.render) and save-time +(ValidateJinja2Template) code paths. They fail on a tree without the safe-empty +default and pass with it. +""" +from changedetectionio.notification_service import NotificationContextData + + +def test_restock_token_present_in_default_context(): + assert 'restock' in NotificationContextData() + + +def test_restock_token_renders_safely_for_non_restock_watch(): + """Send time: a non-restock watch must not crash on {{ restock.price }}.""" + from changedetectionio.jinja2_custom import render as jinja_render + + ctx = NotificationContextData() # a plain, non-restock watch context + rendered = jinja_render(template_str="Price is {{ restock.price }}", **ctx) + # The undefined price renders as empty rather than raising UndefinedError. + assert rendered == "Price is " + + +def test_restock_token_validates_in_system_settings(): + """Save time: a system-wide body using {{ restock.price }} must validate.""" + from changedetectionio.forms import ValidateJinja2Template + + class _Field: + def __init__(self, data): + self.data = data + + # Raised ValidationError before the fix; must not raise now. + ValidateJinja2Template()(None, _Field("Price is {{ restock.price }}"))