From 88f4beb08faf7e9849c45ad5a8166bca7bdbc248 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Tue, 5 May 2026 20:32:14 +1000 Subject: [PATCH] Notifications - extra check for system default #4119 (#4122) --- changedetectionio/notification/handler.py | 3 ++ changedetectionio/notification_service.py | 6 ++-- changedetectionio/tests/test_notification.py | 33 ++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/changedetectionio/notification/handler.py b/changedetectionio/notification/handler.py index 5e664d03..a5f870af 100644 --- a/changedetectionio/notification/handler.py +++ b/changedetectionio/notification/handler.py @@ -65,6 +65,9 @@ def notification_format_align_with_apprise(n_format : str): :return: """ + if not n_format: + return NotifyFormat.TEXT.value + if n_format.startswith('html'): # Apprise only knows 'html' not 'htmlcolor' etc, which shouldnt matter here n_format = NotifyFormat.HTML.value diff --git a/changedetectionio/notification_service.py b/changedetectionio/notification_service.py index c812b1d9..13f3f2d7 100644 --- a/changedetectionio/notification_service.py +++ b/changedetectionio/notification_service.py @@ -29,7 +29,7 @@ def _check_cascading_vars(datastore, var_name, watch): v = watch.get(var_name) if v and not watch.get('notification_muted'): if var_name == 'notification_format' and v == USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: - return datastore.data['settings']['application'].get('notification_format') + return datastore.data['settings']['application'].get('notification_format') or default_notification_format return v @@ -457,7 +457,7 @@ Thanks - Your omniscient changedetection.io installation. 'notification_body': body, 'notification_format': _check_cascading_vars(self.datastore, 'notification_format', watch), }) - n_object['markup_text_links_to_html_links'] = n_object.get('notification_format').startswith('html') + n_object['markup_text_links_to_html_links'] = (n_object.get('notification_format') or '').startswith('html') if len(watch['notification_urls']): n_object['notification_urls'] = watch['notification_urls'] @@ -506,7 +506,7 @@ Thanks - Your omniscient changedetection.io installation. 'notification_body': body, 'notification_format': _check_cascading_vars(self.datastore, 'notification_format', watch), }) - n_object['markup_text_links_to_html_links'] = n_object.get('notification_format').startswith('html') + n_object['markup_text_links_to_html_links'] = (n_object.get('notification_format') or '').startswith('html') if len(watch['notification_urls']): n_object['notification_urls'] = watch['notification_urls'] diff --git a/changedetectionio/tests/test_notification.py b/changedetectionio/tests/test_notification.py index a679485f..0b9e6daf 100644 --- a/changedetectionio/tests/test_notification.py +++ b/changedetectionio/tests/test_notification.py @@ -541,6 +541,39 @@ def test_single_send_test_notification_on_watch(client, live_server, measure_mem assert 'Current snapshot: Example text: example test' in x os.unlink(os.path.join(datastore_path, "notification.txt")) +# Regression test for #4119 - sending a test notification with 'System default' format caused a crash +def test_send_test_notification_with_system_default_format(client, live_server, measure_memory_usage, datastore_path): + + set_original_response(datastore_path=datastore_path) + if os.path.isfile(os.path.join(datastore_path, "notification.txt")): + os.unlink(os.path.join(datastore_path, "notification.txt")) + + test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://') + "?status_code=204" + + test_url = url_for('test_endpoint', _external=True) + uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) + client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) + wait_for_all_checks(client) + + # New watches default to USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH. + # The JS sends this value verbatim from the select; it must not crash. + res = client.post( + url_for("ui.ui_notification.ajax_callback_send_notification_test") + f"/{uuid}", + data={ + "notification_urls": test_notification_url, + "notification_body": default_notification_body, + "notification_title": default_notification_title, + "notification_format": USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH, + }, + follow_redirects=True + ) + + assert res.status_code != 400 + assert res.status_code != 500 + + client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True) + + def _test_color_notifications(client, notification_body_token, datastore_path): set_original_response(datastore_path=datastore_path)