122 lines
3.6 KiB
Python
122 lines
3.6 KiB
Python
"""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"<a>{s}</a>"
|
|
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"),
|
|
("<config><host>db</host><port>5432</port></config>", ".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")
|