Files
resolutionflow/backend/tests/test_script_template_engine.py
chihlasm d4dbf44781 feat: Script Generator Phase 1+2 — backend, engine, API, frontend, template editor, parameter detector
Complete Script Generator feature including:

Backend:
- ScriptCategory, ScriptTemplate, ScriptGeneration models
- ScriptTemplateEngine with substitution, filters, sanitization
- CRUD + share API endpoints with permission checks
- Integration tests for permissions and sharing
- Migration 057 with AD User Management seed templates

Frontend — Script Library:
- Browse templates with category tabs and search
- Configure pane with parameter form and script generation
- Script preview with live substitution and copy/download
- scriptGeneratorStore Zustand store

Frontend — Template Editor:
- Full CRUD form with metadata, script body (Monaco Editor), parameters
- ParameterSchemaBuilder with visual builder + JSON toggle
- ScriptManagePage with routing and nav link

Frontend — Parameter Detector:
- Client-side PowerShell parameter detection engine
- Detects script-level param() blocks and variable assignments
- Type inference from PS type annotations and value patterns
- ParameterDetectorStepper one-by-one review UI with accept/skip

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 20:18:59 -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]"