SurfSense/surfsense_backend/tests/unit/automations/templating/test_render.py
CREDO23 db4eef651f test(automations/templating): lock render, filters, environment, context
render.py (4): variable substitution, StrictUndefined raises on missing
keys, evaluate_predicate coerces to bool, render_value walks dicts/lists
and renders string leaves.

filters.py (4): slugify produces URL-safe output, date formats datetime
with strftime, date(None) → "" so templates can write
{{ inputs.last_fired_at | date }} on first run, date(str) passes through.

environment.py (4): the sandbox boundary — disallowed Jinja built-ins
(e.g. pprint) raise, and the finalize hook coerces non-string outputs
to predictable wire shapes (datetime → ISO, None → "", dict → JSON).

context.py (1): build_run_context exposes {run, inputs, steps} with the
exact shape every plan template body relies on.

13 tests total, all pure unit.
2026-05-28 19:03:22 +02:00

59 lines
2.1 KiB
Python

"""Lock the public template-rendering surface: render, predicate, recursive."""
from __future__ import annotations
import pytest
from jinja2 import UndefinedError
from app.automations.templating.render import (
evaluate_predicate,
render_template,
render_value,
)
pytestmark = pytest.mark.unit
def test_render_template_substitutes_context_variables() -> None:
"""A template referencing a context variable produces the substituted
string. Most basic contract of the template engine."""
result = render_template("Hello {{ name }}!", {"name": "World"})
assert result == "Hello World!"
def test_render_template_raises_on_undefined_variable() -> None:
"""Referencing a variable that isn't in the context raises rather than
rendering the empty string. Locks the StrictUndefined safety net so
template typos surface as run failures instead of silent corruption."""
with pytest.raises(UndefinedError):
render_template("Hello {{ missing }}!", {})
def test_evaluate_predicate_returns_truthy_outcome_of_expression() -> None:
"""``evaluate_predicate`` compiles a Jinja **expression** (not template
body) and coerces the value to ``bool``. Drives ``step.when`` gating."""
assert evaluate_predicate("inputs.count > 0", {"inputs": {"count": 3}}) is True
assert evaluate_predicate("inputs.count > 0", {"inputs": {"count": 0}}) is False
def test_render_value_renders_strings_recursively_through_dicts_and_lists() -> None:
"""``render_value`` walks dicts and lists, renders string leaves through
the template engine, and leaves non-strings untouched. This is the
primitive ``execute_step`` uses to render step params at run time."""
context = {"inputs": {"name": "World"}, "topic": "weekly"}
rendered = render_value(
{
"greeting": "Hello {{ inputs.name }}",
"tags": ["{{ topic }}", "static"],
"config": {"retries": 3, "label": "{{ topic }}-{{ inputs.name }}"},
},
context,
)
assert rendered == {
"greeting": "Hello World",
"tags": ["weekly", "static"],
"config": {"retries": 3, "label": "weekly-World"},
}