233 lines
7.7 KiB
Python
233 lines
7.7 KiB
Python
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
import yaml
|
||
|
||
import jinjaturtle.core as core
|
||
from jinjaturtle.core import (
|
||
detect_format,
|
||
parse_config,
|
||
flatten_config,
|
||
generate_ansible_yaml,
|
||
generate_jinja2_template,
|
||
make_var_name,
|
||
)
|
||
|
||
SAMPLES_DIR = Path(__file__).parent / "samples"
|
||
|
||
|
||
def test_make_var_name_basic():
|
||
# simple sanity checks on the naming rules
|
||
assert (
|
||
make_var_name("jinjaturtle", ("somesection", "foo"))
|
||
== "jinjaturtle_somesection_foo"
|
||
)
|
||
assert (
|
||
make_var_name("JinjaTurtle", ("Other-Section", "some value"))
|
||
== "jinjaturtle_other_section_some_value"
|
||
)
|
||
# no trailing underscores, all lowercase, no spaces
|
||
name = make_var_name("MyRole", (" Section Name ", "Key-Name "))
|
||
assert name == name.lower()
|
||
assert " " not in name
|
||
assert not name.endswith("_")
|
||
|
||
|
||
def test_make_var_name_empty_path_returns_prefix():
|
||
# Cover the branch where there are no path components.
|
||
assert make_var_name("MyRole", ()) == "myrole"
|
||
|
||
|
||
def test_detect_format_explicit_overrides_suffix(tmp_path: Path):
|
||
# Explicit format should win over file suffix.
|
||
cfg_path = tmp_path / "config.ini"
|
||
cfg_path.write_text("[section]\nkey=value\n", encoding="utf-8")
|
||
|
||
fmt = detect_format(cfg_path, explicit="toml")
|
||
assert fmt == "toml"
|
||
|
||
|
||
def test_detect_format_fallback_ini(tmp_path: Path):
|
||
# Unknown suffix should fall back to "ini".
|
||
cfg_path = tmp_path / "weird.cnf"
|
||
cfg_path.write_text("[section]\nkey=value\n", encoding="utf-8")
|
||
|
||
fmt, parsed = parse_config(cfg_path) # no explicit fmt
|
||
assert fmt == "ini"
|
||
# parsed should be an INI ConfigParser with our section/key
|
||
flat = flatten_config(fmt, parsed)
|
||
assert any(path == ("section", "key") for path, _ in flat)
|
||
|
||
|
||
def test_formats_match_expected_extensions():
|
||
"""
|
||
Sanity check that format detection lines up with the filenames
|
||
we’re using for the samples.
|
||
"""
|
||
toml_path = SAMPLES_DIR / "tom.toml"
|
||
ini_path = SAMPLES_DIR / "php.ini"
|
||
xml_path = SAMPLES_DIR / "ossec.xml"
|
||
|
||
fmt_toml, _ = parse_config(toml_path)
|
||
fmt_ini, _ = parse_config(ini_path)
|
||
fmt_xml, _ = parse_config(xml_path)
|
||
|
||
assert fmt_toml == "toml"
|
||
assert fmt_ini == "ini"
|
||
assert fmt_xml == "xml"
|
||
|
||
|
||
def test_parse_config_unsupported_format(tmp_path: Path):
|
||
"""
|
||
Hit the ValueError in parse_config when fmt is not a supported format.
|
||
"""
|
||
cfg_path = tmp_path / "config.whatever"
|
||
cfg_path.write_text("", encoding="utf-8")
|
||
|
||
with pytest.raises(ValueError):
|
||
parse_config(cfg_path, fmt="bogus")
|
||
|
||
|
||
def test_generate_jinja2_template_type_and_format_errors():
|
||
"""
|
||
Exercise the error branches in generate_jinja2_template:
|
||
- toml with non-dict parsed
|
||
- ini with non-ConfigParser parsed
|
||
- yaml with wrong parsed type
|
||
- json with wrong parsed type
|
||
- completely unsupported fmt (with and without original_text)
|
||
"""
|
||
# wrong type for TOML
|
||
with pytest.raises(TypeError):
|
||
generate_jinja2_template("toml", parsed="not a dict", role_prefix="role")
|
||
|
||
# wrong type for INI
|
||
with pytest.raises(TypeError):
|
||
generate_jinja2_template(
|
||
"ini", parsed={"not": "a configparser"}, role_prefix="role"
|
||
)
|
||
|
||
# wrong type for YAML
|
||
with pytest.raises(TypeError):
|
||
generate_jinja2_template("yaml", parsed=None, role_prefix="role")
|
||
|
||
# wrong type for JSON
|
||
with pytest.raises(TypeError):
|
||
generate_jinja2_template("json", parsed=None, role_prefix="role")
|
||
|
||
# unsupported format, no original_text
|
||
with pytest.raises(ValueError):
|
||
generate_jinja2_template("bogusfmt", parsed=None, role_prefix="role")
|
||
|
||
# unsupported format, with original_text
|
||
with pytest.raises(ValueError):
|
||
generate_jinja2_template(
|
||
"bogusfmt",
|
||
parsed=None,
|
||
role_prefix="role",
|
||
original_text="foo=bar",
|
||
)
|
||
|
||
|
||
def test_normalize_default_value_true_false_strings():
|
||
# 'true'/'false' strings should be preserved as strings and double-quoted in YAML.
|
||
flat_items = [
|
||
(("section", "foo"), "true"),
|
||
(("section", "bar"), "FALSE"),
|
||
]
|
||
ansible_yaml = generate_ansible_yaml("role", flat_items)
|
||
data = yaml.safe_load(ansible_yaml)
|
||
assert data["role_section_foo"] == "true"
|
||
assert data["role_section_bar"] == "FALSE"
|
||
|
||
|
||
def test_fallback_str_representer_for_unknown_type():
|
||
"""
|
||
Ensure that the _fallback_str_representer is used for objects that
|
||
PyYAML doesn't know how to represent.
|
||
"""
|
||
|
||
class Weird:
|
||
def __str__(self) -> str:
|
||
return "weird-value"
|
||
|
||
data = {"foo": Weird()}
|
||
|
||
dumped = yaml.dump(
|
||
data,
|
||
Dumper=core._TurtleDumper,
|
||
sort_keys=False,
|
||
default_flow_style=False,
|
||
)
|
||
|
||
# It should serialize without error, and the string form should appear.
|
||
assert "weird-value" in dumped
|
||
|
||
|
||
def test_normalize_default_value_bool_inputs_are_stringified():
|
||
"""
|
||
Boolean values are now preserved as booleans in YAML (not stringified).
|
||
This supports proper type preservation for JSON and other formats.
|
||
"""
|
||
flat_items = [
|
||
(("section", "flag_true"), True),
|
||
(("section", "flag_false"), False),
|
||
]
|
||
ansible_yaml = generate_ansible_yaml("role", flat_items)
|
||
data = yaml.safe_load(ansible_yaml)
|
||
|
||
# Booleans are now preserved as booleans
|
||
assert data["role_section_flag_true"] is True
|
||
assert data["role_section_flag_false"] is False
|
||
|
||
|
||
def test_flatten_config_unsupported_format():
|
||
"""
|
||
Calling flatten_config with an unknown fmt should raise ValueError.
|
||
"""
|
||
with pytest.raises(ValueError) as exc:
|
||
flatten_config("bogusfmt", parsed=None)
|
||
|
||
assert "Unsupported format" in str(exc.value)
|
||
|
||
|
||
def test_make_var_name_collapses_adjacent_separators():
|
||
"""Regression: adjacent separators must not produce a forbidden ``__``.
|
||
|
||
A source key such as ``log..level`` or ``cache--size`` previously sanitised
|
||
to a name containing a double underscore (``..._log__level``). The output
|
||
safety gate in safety.py rejects *any* ``__`` in a generated identifier
|
||
(it is the gateway to Jinja2 SSTI gadgets), so emitting one made JinjaTurtle
|
||
reject its own placeholder and abort generation on entirely benign config.
|
||
make_var_name now collapses runs of underscores to a single ``_``.
|
||
"""
|
||
for raw_key in ("log..level", "cache--size", "a...b", "x.-.y", "a..b--c"):
|
||
name = make_var_name("role", ("main", raw_key))
|
||
assert "__" not in name, f"{raw_key!r} produced {name!r}"
|
||
# Still a valid Ansible/Jinja identifier.
|
||
assert name.replace("_", "").isalnum() or name == "role"
|
||
|
||
# The double-underscore-free collapse is stable and predictable.
|
||
assert make_var_name("role", ("main", "log..level")) == "role_main_log_level"
|
||
assert make_var_name("role", ("main", "cache--size")) == "role_main_cache_size"
|
||
# A leading-digit-free prefix with its own repeated separators is collapsed too.
|
||
assert make_var_name("My__Role", ("k",)) == "my_role_k"
|
||
|
||
|
||
def test_make_var_name_collapsed_names_pass_output_safety_gate():
|
||
"""The names make_var_name emits must be accepted by the safety gate.
|
||
|
||
This binds the two modules together: whatever identifier make_var_name
|
||
produces for a hostile-looking key must lex as the JinjaTurtle subset, so a
|
||
real template built from it is not refused.
|
||
"""
|
||
from jinjaturtle.safety import verify_jinja2_template_safe
|
||
|
||
for raw_key in ("log..level", "cache--size", "a...b", "weird..key..name"):
|
||
var = make_var_name("demo", ("section", raw_key))
|
||
# Build the kind of expression a handler would emit for this variable.
|
||
template = "{{ " + var + " }}"
|
||
# Must not raise TemplateSafetyError.
|
||
verify_jinja2_template_safe(template)
|