Files
changedetection.io/changedetectionio/tests/test_api_jinja2_validation.py
dgtlmoonandClaude Opus 5.5 352b036217
Build and push containers / metadata (push) Canceled after 0s
Build and push containers / build-push-containers (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/amd64 (alpine) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/arm64 (alpine) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/amd64 (main) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/arm/v7 (main) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/arm/v8 (main) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/arm64 (main) (push) Canceled after 0s
ChangeDetection.io App Test / lint-code (push) Canceled after 0s
ChangeDetection.io App Test / lint-translations (push) Canceled after 0s
ChangeDetection.io App Test / lint-template-i18n (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-11 (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-12 (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-13 (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-14 (push) Canceled after 0s
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Canceled after 0s
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built package works basically. (push) Canceled after 0s
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Canceled after 0s
API - Reject broken Jinja2 templates in url, link_to_open, body and headers (shared with the edit form) (#4587)
* API - Reject broken Jinja2 in url, link_to_open, body and headers, shared validation with the edit form

The edit form validated Jinja2 templates on save but the API did not, so a broken body or
header template saved through the API only failed at fetch time, on every check.

- New jinja2_custom/validate.py: validate_jinja2_fields() renders each value that matches
  JINJA2_MARKER_PATTERN and returns {field: translated error}
- The edit form uses it instead of four copies of the same try/except
- API create and update return 400 naming the field

Translations: messages moved to the new file (msgids unchanged); also translated the header
error for fr/it and fixed the cs/es wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* rebuild

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:15:52 +02:00

76 lines
3.0 KiB
Python

"""
The API rejects broken Jinja2 in url, link_to_open, body and headers, the same as the edit form.
Before, a broken template saved through the API was only found at fetch time and the watch then
failed on every check.
"""
import json
from flask import url_for
from .util import delete_all_watches
def _headers(live_server):
api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
return {'x-api-key': api_key, 'content-type': 'application/json'}
def test_api_create_rejects_broken_jinja2(client, live_server, measure_memory_usage, datastore_path):
test_url = url_for('test_endpoint', _external=True)
headers = _headers(live_server)
res = client.post(url_for("createwatch"), headers=headers,
data=json.dumps({"url": test_url, "method": "POST", "body": "{{ unclosed"}))
assert res.status_code == 400
assert b"body: Invalid template syntax" in res.data
res = client.post(url_for("createwatch"), headers=headers,
data=json.dumps({"url": test_url, "headers": {"x-bad": "{% endfor %}"}}))
assert res.status_code == 400
assert '"x-bad" header' in res.get_json()
res = client.post(url_for("createwatch"), headers=headers,
data=json.dumps({"url": test_url + "?q={{ broken("}))
assert res.status_code == 400
assert b"url: Invalid template syntax" in res.data
assert len(live_server.app.config['DATASTORE'].data['watching']) == 0
# Valid templates and text that only looks like Jinja2 are accepted
res = client.post(url_for("createwatch"), headers=headers,
data=json.dumps({"url": test_url, "method": "POST",
"body": 'q={%22key%22:1}',
"headers": {"x-ok": "{{ 2*2 }}"}}))
assert res.status_code == 201, res.data
delete_all_watches(client)
def test_api_update_rejects_broken_jinja2(client, live_server, measure_memory_usage, datastore_path):
test_url = url_for('test_endpoint', _external=True)
headers = _headers(live_server)
uuid = live_server.app.config['DATASTORE'].add_watch(url=test_url)
res = client.put(url_for("watch", uuid=uuid), headers=headers,
data=json.dumps({"method": "POST", "body": "{{ unclosed"}))
assert res.status_code == 400
assert b"body: Invalid template syntax" in res.data
res = client.put(url_for("watch", uuid=uuid), headers=headers,
data=json.dumps({"link_to_open": "https://example.com/{% if %}"}))
assert res.status_code == 400
assert b"link_to_open: Invalid template syntax" in res.data
watch = live_server.app.config['DATASTORE'].data['watching'][uuid]
assert not watch.get('body')
assert not watch.get('link_to_open')
res = client.put(url_for("watch", uuid=uuid), headers=headers,
data=json.dumps({"method": "POST", "body": 'q={%22key%22:1}'}))
assert res.status_code == 200, res.data
assert watch.get('body') == 'q={%22key%22:1}'
delete_all_watches(client)