"""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")