"""Tests for ScriptTemplateEngine — parameter substitution, sanitization, and filters.""" import pytest from app.services.script_template_engine import ScriptTemplateEngine, ScriptRenderError @pytest.fixture def engine(): return ScriptTemplateEngine() # ── Basic substitution ──────────────────────────────────────────────────── def test_simple_substitution(engine): body = "New-ADUser -Name '{{ first_name }} {{ last_name }}'" result = engine.render(body, {"first_name": "John", "last_name": "Smith"}) assert result == "New-ADUser -Name 'John Smith'" def test_missing_required_param_raises(engine): body = "New-ADUser -Name '{{ first_name }}'" with pytest.raises(ScriptRenderError, match="first_name"): engine.render(body, {}) def test_extra_params_ignored(engine): body = "New-ADUser -Name '{{ first_name }}'" result = engine.render(body, {"first_name": "John", "extra": "ignored"}) assert result == "New-ADUser -Name 'John'" # ── Security: single-quote injection ───────────────────────────────────── def test_single_quote_in_value_is_escaped(engine): body = "Set-ADUser -Name '{{ name }}'" result = engine.render(body, {"name": "O'Brien"}) # Single quotes doubled for PowerShell safety assert "O''Brien" in result def test_backtick_in_value_is_escaped(engine): body = "Write-Host '{{ msg }}'" result = engine.render(body, {"msg": "hello`world"}) assert "`" not in result or "``" in result # backtick is escaped def test_dollar_sign_in_value_is_escaped(engine): body = "Write-Host '{{ msg }}'" result = engine.render(body, {"msg": "price is $100"}) # Dollar sign escaped so it doesn't interpolate as a PowerShell variable assert "`$100" in result or "'price is $100'" in result # ── Filters ────────────────────────────────────────────────────────────── def test_as_secure_string_filter(engine): body = "$secPwd = {{ password | as_secure_string }}" result = engine.render(body, {"password": "MyP@ss123"}) assert "ConvertTo-SecureString" in result assert "MyP@ss123" in result assert "-AsPlainText -Force" in result def test_as_array_filter(engine): body = "$groups = @({{ groups | as_array }})" result = engine.render(body, {"groups": ["GroupA", "GroupB"]}) assert "'GroupA','GroupB'" in result def test_as_array_filter_single_item(engine): body = "$groups = @({{ groups | as_array }})" result = engine.render(body, {"groups": ["OnlyGroup"]}) assert "'OnlyGroup'" in result def test_as_bool_filter_true(engine): body = "$force = {{ force_change | as_bool }}" result = engine.render(body, {"force_change": True}) assert "$true" in result def test_as_bool_filter_false(engine): body = "$force = {{ force_change | as_bool }}" result = engine.render(body, {"force_change": False}) assert "$false" in result # ── Conditional blocks ─────────────────────────────────────────────────── def test_if_block_included_when_truthy(engine): body = "{% if groups %}\nAdd-Groups\n{% endif %}" result = engine.render(body, {"groups": ["GroupA"]}) assert "Add-Groups" in result def test_if_block_excluded_when_falsy(engine): body = "{% if groups %}\nAdd-Groups\n{% endif %}" result = engine.render(body, {"groups": []}) assert "Add-Groups" not in result def test_if_block_excluded_when_missing(engine): body = "{% if groups %}\nAdd-Groups\n{% endif %}" result = engine.render(body, {}) assert "Add-Groups" not in result # ── Parameter redaction ────────────────────────────────────────────────── def test_sensitive_params_redacted_in_record(engine): params = {"first_name": "John", "password": "Secret123"} sensitive_keys = {"password"} redacted = engine.redact_sensitive(params, sensitive_keys) assert redacted["first_name"] == "John" assert redacted["password"] == "[REDACTED]"