98 lines
3 KiB
Python
98 lines
3 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
from defusedxml.common import DTDForbidden
|
|
|
|
from jinjaturtle.core import (
|
|
VariableNameCollisionError,
|
|
generate_ansible_yaml,
|
|
parse_config,
|
|
)
|
|
from jinjaturtle.handlers.xml import XmlHandler
|
|
from jinjaturtle.multi import iter_supported_files, process_directory
|
|
|
|
|
|
def test_defaults_string_values_are_ansible_unsafe():
|
|
output = generate_ansible_yaml(
|
|
"role", [(("section", "key"), "{{ lookup('pipe', 'id') }}")]
|
|
)
|
|
assert "!unsafe" in output
|
|
assert yaml.safe_load(output)["role_section_key"] == "{{ lookup('pipe', 'id') }}"
|
|
|
|
|
|
def test_variable_name_collisions_fail_closed():
|
|
with pytest.raises(VariableNameCollisionError):
|
|
generate_ansible_yaml(
|
|
"role",
|
|
[
|
|
(("section", "a-b"), "safe"),
|
|
(("section", "a_b"), "evil"),
|
|
],
|
|
)
|
|
|
|
|
|
def test_folder_mode_does_not_follow_file_symlinks(tmp_path: Path):
|
|
outside = tmp_path / "outside.ini"
|
|
outside.write_text("[s]\nsecret = value\n", encoding="utf-8")
|
|
root = tmp_path / "root"
|
|
root.mkdir()
|
|
(root / "link.ini").symlink_to(outside)
|
|
(root / "real.ini").write_text("[s]\nname = ok\n", encoding="utf-8")
|
|
|
|
files = iter_supported_files(root, recursive=True)
|
|
assert files == [root / "real.ini"]
|
|
defaults, _outputs = process_directory(root, recursive=True, role_prefix="role")
|
|
assert "secret" not in defaults
|
|
assert "real.ini" in defaults
|
|
|
|
|
|
def test_xml_parser_rejects_dtd_and_entities(tmp_path: Path):
|
|
xml_path = tmp_path / "evil.xml"
|
|
xml_path.write_text('<!DOCTYPE r [<!ENTITY x "boom">]><r>&x;</r>', encoding="utf-8")
|
|
|
|
with pytest.raises(DTDForbidden):
|
|
parse_config(xml_path)
|
|
|
|
|
|
def test_xml_source_comments_cannot_forge_internal_markers():
|
|
source = (
|
|
"<root><!--IF:role_missing--><name>ok</name><!--ENDIF:role_missing--></root>"
|
|
)
|
|
template = XmlHandler().generate_jinja2_template(None, "role", original_text=source)
|
|
|
|
assert "{% if role_missing is defined %}" not in template
|
|
assert "{% endif %}" not in template
|
|
assert "IF:role_missing" in template
|
|
assert "ENDIF:role_missing" in template
|
|
|
|
|
|
def test_cli_refuses_to_write_output_symlink(tmp_path: Path):
|
|
import subprocess
|
|
import sys
|
|
|
|
src = tmp_path / "input.ini"
|
|
src.write_text("[s]\nname = ok\n", encoding="utf-8")
|
|
target = tmp_path / "target.yml"
|
|
target.write_text("do not overwrite\n", encoding="utf-8")
|
|
link = tmp_path / "defaults.yml"
|
|
link.symlink_to(target)
|
|
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"jinjaturtle.cli",
|
|
str(src),
|
|
"-d",
|
|
str(link),
|
|
],
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
env={"PYTHONPATH": str(Path(__file__).resolve().parents[1] / "src")},
|
|
check=False,
|
|
)
|
|
|
|
assert result.returncode != 0
|
|
assert target.read_text(encoding="utf-8") == "do not overwrite\n"
|