"""Regression tests for the deeply-nested-input denial-of-service fix.
Pathologically nested configuration (thousands of container levels) used to
drive the recursive walkers in ``core`` -- or the underlying parser itself --
past Python's stack limit, raising an uncaught ``RecursionError``. The fix
bounds nesting depth and converts both cases into a clean, catchable
``ConfigTooDeeplyNestedError``. These tests pin that behaviour and guard
against regressions, while confirming normally-nested configs still process.
"""
from __future__ import annotations
import tempfile
from pathlib import Path
import pytest
from jinjaturtle.core import (
ConfigTooDeeplyNestedError,
MAX_CONFIG_DEPTH,
analyze_loops,
flatten_config,
generate_ansible_yaml,
generate_jinja2_template,
parse_config,
)
def _run_pipeline(content: str, suffix: str) -> None:
with tempfile.NamedTemporaryFile(
"w", suffix=suffix, delete=False, encoding="utf-8"
) as tf:
tf.write(content)
path = Path(tf.name)
try:
fmt, parsed = parse_config(path)
loops = analyze_loops(fmt, parsed)
flat = flatten_config(fmt, parsed, loops)
generate_jinja2_template(
fmt, parsed, "role", original_text=content, loop_candidates=loops
)
generate_ansible_yaml("role", flat, loops)
finally:
path.unlink()
def _deep_json_objects(depth: int) -> str:
s = "0"
for _ in range(depth):
s = '{"a":' + s + "}"
return s
def _deep_json_arrays(depth: int) -> str:
return "[" * depth + "1" + "]" * depth
def _deep_xml(depth: int) -> str:
s = "v"
for _ in range(depth):
s = f"{s}"
return s
def _deep_yaml(depth: int) -> str:
s = "v"
for _ in range(depth):
s = "{a: " + s + "}"
return s
def _deep_toml(depth: int) -> str:
return "a = " + "[" * depth + "1" + "]" * depth
@pytest.mark.parametrize(
"content, suffix",
[
(_deep_json_objects(5000), ".json"),
(_deep_json_arrays(100000), ".json"),
(_deep_xml(5000), ".xml"),
(_deep_yaml(3000), ".yaml"),
(_deep_toml(2000), ".toml"),
],
)
def test_deeply_nested_input_is_rejected_cleanly(content: str, suffix: str) -> None:
# Must raise our typed error, and crucially must NOT raise RecursionError
# (pytest would report that as an error, but be explicit about intent).
with pytest.raises(ConfigTooDeeplyNestedError):
_run_pipeline(content, suffix)
def test_recursion_error_never_escapes() -> None:
# Belt-and-suspenders: ensure a RecursionError is never what surfaces.
content = _deep_yaml(6000)
try:
_run_pipeline(content, ".yaml")
except ConfigTooDeeplyNestedError:
pass
except RecursionError: # pragma: no cover - this is the bug we fixed
pytest.fail("RecursionError escaped instead of ConfigTooDeeplyNestedError")
@pytest.mark.parametrize(
"content, suffix",
[
('{"name":"web","port":8080,"tags":["a","b"]}', ".json"),
("db5432", ".xml"),
("name: web\nport: 8080\nnested:\n a:\n b: 1\n", ".yaml"),
('[section]\nkey = "value"\nnums = [1, 2, 3]\n', ".toml"),
("[section]\nkey = value\n", ".ini"),
],
)
def test_normal_configs_still_process(content: str, suffix: str) -> None:
# No exception expected for ordinary, shallow configurations.
_run_pipeline(content, suffix)
def test_just_under_limit_is_accepted() -> None:
# A structure comfortably under the cap must still process successfully.
content = _deep_json_objects(MAX_CONFIG_DEPTH - 10)
_run_pipeline(content, ".json")