From 0c6931c07c933f265daa6c3853458f57d4636f59 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Fri, 13 Feb 2026 14:40:43 +0100 Subject: [PATCH] WIP --- changedetectionio/api/Watch.py | 13 ++++++ changedetectionio/model/__init__.py | 1 + changedetectionio/tests/test_api.py | 8 ++-- changedetectionio/tests/test_api_tags.py | 55 ++++++++++++++++++++++++ docs/api-spec.yaml | 3 ++ 5 files changed, 76 insertions(+), 4 deletions(-) diff --git a/changedetectionio/api/Watch.py b/changedetectionio/api/Watch.py index 390c6f2d0..b0e56f140 100644 --- a/changedetectionio/api/Watch.py +++ b/changedetectionio/api/Watch.py @@ -188,6 +188,19 @@ class Watch(Resource): for field in fields_to_ignore: json_data.pop(field, None) + # Validate remaining fields - reject truly unknown fields + # Get valid fields from WatchBase schema + from . import get_watch_schema_properties + valid_fields = set(get_watch_schema_properties().keys()) + + # Also allow last_viewed (explicitly defined in UpdateWatch schema) + valid_fields.add('last_viewed') + + # Check for unknown fields + unknown_fields = set(json_data.keys()) - valid_fields + if unknown_fields: + return f"Unknown field(s): {', '.join(sorted(unknown_fields))}", 400 + # Update watch with regular (non-processor-config) fields watch.update(json_data) watch.commit() diff --git a/changedetectionio/model/__init__.py b/changedetectionio/model/__init__.py index d54421ee3..c3192bf60 100644 --- a/changedetectionio/model/__init__.py +++ b/changedetectionio/model/__init__.py @@ -26,6 +26,7 @@ class watch_base(dict): - Configuration override chain resolution (Watch → Tag → Global) - Immutability options - Better testing + - USE https://docs.pydantic.dev/latest/integrations/datamodel_code_generator TO BUILD THE MODEL FROM THE API-SPEC!!! CHAIN RESOLUTION ARCHITECTURE: The dream is a 3-level override hierarchy: diff --git a/changedetectionio/tests/test_api.py b/changedetectionio/tests/test_api.py index 2f38d7668..8a2e1b776 100644 --- a/changedetectionio/tests/test_api.py +++ b/changedetectionio/tests/test_api.py @@ -529,12 +529,12 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage, datasto ) assert res.status_code == 400, "Should get error 400 when we give a field that doesnt exist" - # OpenAPI validation message changed when we switched from flask-expects-json to OpenAPI validation - # Using unevaluatedProperties instead of additionalProperties changes the message - assert (b'Additional properties are not allowed' in res.data or + # Backend validation now rejects unknown fields with a clear error message + assert (b'Unknown field' in res.data or + b'Additional properties are not allowed' in res.data or b'Unevaluated properties are not allowed' in res.data or b'does not match any of the regexes' in res.data), \ - "Should reject unknown fields with schema validation error" + "Should reject unknown fields with validation error" # Try a XSS URL diff --git a/changedetectionio/tests/test_api_tags.py b/changedetectionio/tests/test_api_tags.py index 5a5253939..8622fcf80 100644 --- a/changedetectionio/tests/test_api_tags.py +++ b/changedetectionio/tests/test_api_tags.py @@ -176,4 +176,59 @@ def test_api_tags_listing(client, live_server, measure_memory_usage, datastore_p assert res.status_code == 204 +def test_roundtrip_API(client, live_server, measure_memory_usage, datastore_path): + """ + Test the full round trip, this way we test the default Model fits back into OpenAPI spec + :param client: + :param live_server: + :param measure_memory_usage: + :param datastore_path: + :return: + """ + api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') + set_original_response(datastore_path=datastore_path) + + res = client.post( + url_for("tag"), + data=json.dumps({"title": "My tag title"}), + headers={'content-type': 'application/json', 'x-api-key': api_key} + ) + assert res.status_code == 201 + + uuid = res.json.get('uuid') + + # Now fetch it and send it back + + res = client.get( + url_for("tag", uuid=uuid), + headers={'x-api-key': api_key} + ) + + tag = res.json + + tag['last_changed'] = 454444444444 + tag['date_created'] = 454444444444 + + # HTTP PUT ( UPDATE an existing watch ) + res = client.put( + url_for("tag", uuid=uuid), + headers={'x-api-key': api_key, 'content-type': 'application/json'}, + data=json.dumps(tag), + ) + if res.status_code != 200: + print(f"\n=== PUT failed with {res.status_code} ===") + print(f"Error: {res.data}") + assert res.status_code == 200, "HTTP PUT update was sent OK" + + res = client.get( + url_for("watch", uuid=uuid), + headers={'x-api-key': api_key} + ) + last_changed = res.json.get('last_changed') + assert last_changed != 454444444444 + assert last_changed != "454444444444" + + date_created = res.json.get('date_created') + assert date_created != 454444444444 + assert date_created != "454444444444" diff --git a/docs/api-spec.yaml b/docs/api-spec.yaml index 13837f1a2..74407139d 100644 --- a/docs/api-spec.yaml +++ b/docs/api-spec.yaml @@ -178,6 +178,9 @@ components: type: [string, 'null'] description: Proxy configuration maxLength: 5000 + ignore_status_codes: + type: [boolean, 'null'] + description: Ignore HTTP status code errors (boolean or null) webdriver_delay: type: [integer, 'null'] description: Delay in seconds for webdriver