Files
resolutionflow/backend/tests/test_script_template_engine.py
2026-03-13 00:17:14 -04:00

115 lines
4.3 KiB
Python

"""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]"