From 582232b02fdf0e3f3254453766aa0c83dcb84710 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Tue, 16 Sep 2025 17:14:09 +0200 Subject: [PATCH] Add missing val --- changedetectionio/api/Watch.py | 39 +++++++++++++++++++ changedetectionio/api/api_schema.py | 6 +++ .../tests/test_add_replace_remove_filter.py | 6 ++- changedetectionio/tests/test_auth.py | 2 +- changedetectionio/tests/test_css_selector.py | 8 ++-- changedetectionio/tests/test_group.py | 3 +- changedetectionio/tests/test_request.py | 27 ++++++++----- .../tests/test_watch_fields_storage.py | 3 +- .../tests/test_xpath_selector.py | 31 ++++++++------- docs/api-spec.yaml | 4 ++ 10 files changed, 99 insertions(+), 30 deletions(-) diff --git a/changedetectionio/api/Watch.py b/changedetectionio/api/Watch.py index 41fc12da6..57a6d5dd5 100644 --- a/changedetectionio/api/Watch.py +++ b/changedetectionio/api/Watch.py @@ -14,6 +14,35 @@ import copy from . import schema, schema_create_watch, schema_update_watch, validate_openapi_request +def validate_time_between_check_required(json_data): + """ + Validate that at least one time interval is specified when not using default settings. + Returns None if valid, or error message string if invalid. + Defaults to using global settings if time_between_check_use_default is not provided. + """ + # Default to using global settings if not specified + use_default = json_data.get('time_between_check_use_default', True) + + # If using default settings, no validation needed + if use_default: + return None + + # Check if time_between_check exists and has at least one non-zero value + time_check = json_data.get('time_between_check') + if time_check: + if any([ + (time_check.get('weeks') or 0) > 0, + (time_check.get('days') or 0) > 0, + (time_check.get('hours') or 0) > 0, + (time_check.get('minutes') or 0) > 0, + (time_check.get('seconds') or 0) > 0 + ]): + return None + + # If we get here, validation failed + return "At least one time interval (weeks, days, hours, minutes, or seconds) must be specified when not using global settings." + + class Watch(Resource): def __init__(self, **kwargs): # datastore is a black box dependency @@ -81,6 +110,11 @@ class Watch(Resource): if not request.json.get('proxy') in plist: return "Invalid proxy choice, currently supported proxies are '{}'".format(', '.join(plist)), 400 + # Validate time_between_check when not using defaults + validation_error = validate_time_between_check_required(request.json) + if validation_error: + return validation_error, 400 + watch.update(request.json) return "OK", 200 @@ -196,6 +230,11 @@ class CreateWatch(Resource): if not json_data.get('proxy') in plist: return "Invalid proxy choice, currently supported proxies are '{}'".format(', '.join(plist)), 400 + # Validate time_between_check when not using defaults + validation_error = validate_time_between_check_required(json_data) + if validation_error: + return validation_error, 400 + extras = copy.deepcopy(json_data) # Because we renamed 'tag' to 'tags' but don't want to change the API (can do this in v2 of the API) diff --git a/changedetectionio/api/api_schema.py b/changedetectionio/api/api_schema.py index ac948675f..2ef10f671 100644 --- a/changedetectionio/api/api_schema.py +++ b/changedetectionio/api/api_schema.py @@ -119,6 +119,12 @@ def build_watch_json_schema(d): schema['properties']['time_between_check'] = build_time_between_check_json_schema() + schema['properties']['time_between_check_use_default'] = { + "type": "boolean", + "default": True, + "description": "Whether to use global settings for time between checks - defaults to true if not set" + } + schema['properties']['browser_steps'] = { "anyOf": [ { diff --git a/changedetectionio/tests/test_add_replace_remove_filter.py b/changedetectionio/tests/test_add_replace_remove_filter.py index 3ca5284e4..c3479e975 100644 --- a/changedetectionio/tests/test_add_replace_remove_filter.py +++ b/changedetectionio/tests/test_add_replace_remove_filter.py @@ -61,7 +61,8 @@ def test_check_removed_line_contains_trigger(client, live_server, measure_memory data={"trigger_text": 'The golden line', "url": test_url, 'fetch_backend': "html_requests", - 'filter_text_removed': 'y'}, + 'filter_text_removed': 'y', + "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data @@ -154,7 +155,8 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa 'processor': 'text_json_diff', 'fetch_backend': "html_requests", 'filter_text_removed': '', - 'filter_text_added': 'y'}, + 'filter_text_added': 'y', + "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data diff --git a/changedetectionio/tests/test_auth.py b/changedetectionio/tests/test_auth.py index a98599619..2c74b423f 100644 --- a/changedetectionio/tests/test_auth.py +++ b/changedetectionio/tests/test_auth.py @@ -23,7 +23,7 @@ def test_basic_auth(client, live_server, measure_memory_usage): # Check form validation res = client.post( url_for("ui.ui_edit.edit_page", uuid="first"), - data={"include_filters": "", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"}, + data={"include_filters": "", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data diff --git a/changedetectionio/tests/test_css_selector.py b/changedetectionio/tests/test_css_selector.py index bad181c25..1cbd6c436 100644 --- a/changedetectionio/tests/test_css_selector.py +++ b/changedetectionio/tests/test_css_selector.py @@ -95,7 +95,7 @@ def test_check_markup_include_filters_restriction(client, live_server, measure_m # Add our URL to the import page res = client.post( url_for("ui.ui_edit.edit_page", uuid="first"), - data={"include_filters": include_filters, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"}, + data={"include_filters": include_filters, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data @@ -154,7 +154,8 @@ def test_check_multiple_filters(client, live_server, measure_memory_usage): "url": test_url, "tags": "", "headers": "", - 'fetch_backend': "html_requests"}, + 'fetch_backend': "html_requests", + "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data @@ -208,7 +209,8 @@ def test_filter_is_empty_help_suggestion(client, live_server, measure_memory_usa "url": test_url, "tags": "", "headers": "", - 'fetch_backend': "html_requests"}, + 'fetch_backend': "html_requests", + "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data diff --git a/changedetectionio/tests/test_group.py b/changedetectionio/tests/test_group.py index f8791da82..5e2596c54 100644 --- a/changedetectionio/tests/test_group.py +++ b/changedetectionio/tests/test_group.py @@ -424,7 +424,8 @@ def test_order_of_filters_tag_filter_and_watch_filter(client, live_server, measu "url": test_url, "tags": "test-tag-keep-order", "headers": "", - 'fetch_backend': "html_requests"}, + 'fetch_backend': "html_requests", + "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data diff --git a/changedetectionio/tests/test_request.py b/changedetectionio/tests/test_request.py index 85b006338..f4f56fabf 100644 --- a/changedetectionio/tests/test_request.py +++ b/changedetectionio/tests/test_request.py @@ -44,7 +44,8 @@ def test_headers_in_request(client, live_server, measure_memory_usage): "url": test_url, "tags": "", "fetch_backend": 'html_webdriver' if os.getenv('PLAYWRIGHT_DRIVER_URL') else 'html_requests', - "headers": "jinja2:{{ 1+1 }}\nxxx:ooo\ncool:yeah\r\ncookie:"+cookie_header}, + "headers": "jinja2:{{ 1+1 }}\nxxx:ooo\ncool:yeah\r\ncookie:"+cookie_header, + "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data @@ -109,7 +110,8 @@ def test_body_in_request(client, live_server, measure_memory_usage): "tags": "", "method": "POST", "fetch_backend": "html_requests", - "body": "something something"}, + "body": "something something", + "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data @@ -126,7 +128,8 @@ def test_body_in_request(client, live_server, measure_memory_usage): "tags": "", "method": "POST", "fetch_backend": "html_requests", - "body": body_value}, + "body": body_value, + "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data @@ -172,7 +175,8 @@ def test_body_in_request(client, live_server, measure_memory_usage): "tags": "", "method": "GET", "fetch_backend": "html_requests", - "body": "invalid"}, + "body": "invalid", + "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Body must be empty when Request Method is set to GET" in res.data @@ -211,7 +215,8 @@ def test_method_in_request(client, live_server, measure_memory_usage): "url": test_url, "tags": "", "fetch_backend": "html_requests", - "method": "invalid"}, + "method": "invalid", + "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Not a valid choice" in res.data @@ -223,7 +228,8 @@ def test_method_in_request(client, live_server, measure_memory_usage): "url": test_url, "tags": "", "fetch_backend": "html_requests", - "method": "PATCH"}, + "method": "PATCH", + "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data @@ -297,7 +303,8 @@ def test_ua_global_override(client, live_server, measure_memory_usage): "tags": "testtag", "fetch_backend": 'html_requests', # Important - also test case-insensitive - "headers": "User-AGent: agent-from-watch"}, + "headers": "User-AGent: agent-from-watch", + "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data @@ -365,7 +372,8 @@ def test_headers_textfile_in_request(client, live_server, measure_memory_usage): "url": test_url, "tags": "testtag", "fetch_backend": 'html_webdriver' if os.getenv('PLAYWRIGHT_DRIVER_URL') else 'html_requests', - "headers": "xxx:ooo\ncool:yeah\r\n"}, + "headers": "xxx:ooo\ncool:yeah\r\n", + "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data @@ -440,7 +448,8 @@ def test_headers_validation(client, live_server): data={ "url": test_url, "fetch_backend": 'html_requests', - "headers": "User-AGent agent-from-watch\r\nsadfsadfsadfsdaf\r\n:foobar"}, + "headers": "User-AGent agent-from-watch\r\nsadfsadfsadfsdaf\r\n:foobar", + "time_between_check_use_default": "y"}, follow_redirects=True ) diff --git a/changedetectionio/tests/test_watch_fields_storage.py b/changedetectionio/tests/test_watch_fields_storage.py index 506722fe2..c0c264262 100644 --- a/changedetectionio/tests/test_watch_fields_storage.py +++ b/changedetectionio/tests/test_watch_fields_storage.py @@ -28,7 +28,8 @@ def test_check_watch_field_storage(client, live_server, measure_memory_usage): "url": test_url, "tags": "woohoo", "headers": "curl:foo", - 'fetch_backend': "html_requests" + 'fetch_backend': "html_requests", + "time_between_check_use_default": "y" }, follow_redirects=True ) diff --git a/changedetectionio/tests/test_xpath_selector.py b/changedetectionio/tests/test_xpath_selector.py index fbdf201c6..a1d203c25 100644 --- a/changedetectionio/tests/test_xpath_selector.py +++ b/changedetectionio/tests/test_xpath_selector.py @@ -92,7 +92,7 @@ def test_check_xpath_filter_utf8(client, live_server, measure_memory_usage): wait_for_all_checks(client) res = client.post( url_for("ui.ui_edit.edit_page", uuid="first"), - data={"include_filters": filter, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"}, + data={"include_filters": filter, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data @@ -146,7 +146,7 @@ def test_check_xpath_text_function_utf8(client, live_server, measure_memory_usag wait_for_all_checks(client) res = client.post( url_for("ui.ui_edit.edit_page", uuid="first"), - data={"include_filters": filter, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"}, + data={"include_filters": filter, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data @@ -188,7 +188,7 @@ def test_check_markup_xpath_filter_restriction(client, live_server, measure_memo # Add our URL to the import page res = client.post( url_for("ui.ui_edit.edit_page", uuid="first"), - data={"include_filters": xpath_filter, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"}, + data={"include_filters": xpath_filter, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"Updated watch." in res.data @@ -226,7 +226,7 @@ def test_xpath_validation(client, live_server, measure_memory_usage): res = client.post( url_for("ui.ui_edit.edit_page", uuid="first"), - data={"include_filters": "/something horrible", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"}, + data={"include_filters": "/something horrible", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"is not a valid XPath expression" in res.data @@ -247,7 +247,7 @@ def test_xpath23_prefix_validation(client, live_server, measure_memory_usage): res = client.post( url_for("ui.ui_edit.edit_page", uuid="first"), - data={"include_filters": "xpath:/something horrible", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"}, + data={"include_filters": "xpath:/something horrible", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"is not a valid XPath expression" in res.data @@ -298,7 +298,7 @@ def test_xpath1_lxml(client, live_server, measure_memory_usage): res = client.post( url_for("ui.ui_edit.edit_page", uuid="first"), data={"include_filters": "xpath1://title/text()", "url": test_url, "tags": "", "headers": "", - 'fetch_backend': "html_requests"}, + 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) @@ -331,7 +331,7 @@ def test_xpath1_validation(client, live_server, measure_memory_usage): res = client.post( url_for("ui.ui_edit.edit_page", uuid="first"), - data={"include_filters": "xpath1:/something horrible", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"}, + data={"include_filters": "xpath1:/something horrible", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) assert b"is not a valid XPath expression" in res.data @@ -359,7 +359,7 @@ def test_check_with_prefix_include_filters(client, live_server, measure_memory_u res = client.post( url_for("ui.ui_edit.edit_page", uuid="first"), data={"include_filters": "xpath://*[contains(@class, 'sametext')]", "url": test_url, "tags": "", "headers": "", - 'fetch_backend': "html_requests"}, + 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True ) @@ -413,7 +413,8 @@ def test_various_rules(client, live_server, measure_memory_usage): "url": test_url, "tags": "", "headers": "", - 'fetch_backend': "html_requests"}, + 'fetch_backend': "html_requests", + "time_between_check_use_default": "y"}, follow_redirects=True ) wait_for_all_checks(client) @@ -444,7 +445,8 @@ def test_xpath_20(client, live_server, measure_memory_usage): "url": test_url, "tags": "", "headers": "", - 'fetch_backend': "html_requests"}, + 'fetch_backend': "html_requests", + "time_between_check_use_default": "y"}, follow_redirects=True ) @@ -481,7 +483,8 @@ def test_xpath_20_function_count(client, live_server, measure_memory_usage): "url": test_url, "tags": "", "headers": "", - 'fetch_backend': "html_requests"}, + 'fetch_backend': "html_requests", + "time_between_check_use_default": "y"}, follow_redirects=True ) @@ -517,7 +520,8 @@ def test_xpath_20_function_count2(client, live_server, measure_memory_usage): "url": test_url, "tags": "", "headers": "", - 'fetch_backend': "html_requests"}, + 'fetch_backend': "html_requests", + "time_between_check_use_default": "y"}, follow_redirects=True ) @@ -554,7 +558,8 @@ def test_xpath_20_function_string_join_matches(client, live_server, measure_memo "url": test_url, "tags": "", "headers": "", - 'fetch_backend': "html_requests"}, + 'fetch_backend': "html_requests", + "time_between_check_use_default": "y"}, follow_redirects=True ) diff --git a/docs/api-spec.yaml b/docs/api-spec.yaml index 27bc6bcce..eee9e592c 100644 --- a/docs/api-spec.yaml +++ b/docs/api-spec.yaml @@ -188,6 +188,10 @@ components: seconds: type: integer description: Time intervals between checks + time_between_check_use_default: + type: boolean + default: true + description: Whether to use global settings for time between checks - defaults to true if not set notification_urls: type: array items: