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(']>&x;', encoding="utf-8") with pytest.raises(DTDForbidden): parse_config(xml_path) def test_xml_source_comments_cannot_forge_internal_markers(): source = ( "ok" ) 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"