algo/tests/unit/test_double_templating.py
Dan Guido 4bb13a5ce8
Fix Ansible 12 double-templating and Jinja2 spacing issues (#14836)
* Fix Ansible 12 double-templating and Jinja2 spacing issues

This PR fixes critical deployment issues and improves code consistency for Ansible 12 compatibility.

## Fixed Issues

### 1. Double-templating bug (Issue #14835)
Fixed 7 instances of invalid double-templating that breaks deployments:
- Changed `{{ lookup('file', '{{ var }}') }}` to `{{ lookup('file', var) }}`
- Affects Azure, DigitalOcean, GCE, Linode, and IPsec configurations
- Added comprehensive test to prevent regression

### 2. Jinja2 spacing inconsistencies
Fixed 33+ spacing issues for better code quality:
- Removed spaces between Jinja2 blocks: `}} {%` → `}}{%`
- Fixed operator spacing: `int -1` → `int - 1`
- Fixed filter spacing: `|b64encode` → `| b64encode`
- Consolidated multiline expressions to single lines

### 3. Test suite improvements
Enhanced boolean type checking test to be more targeted:
- Excludes external dependencies and CloudFormation templates
- Only tests Algo's actual codebase
- Verified with mutation testing
- Added comprehensive documentation

## Testing
- All 87 unit tests pass
- 0 Jinja2 spacing issues remaining (verified by ansible-lint)
- Ansible syntax checks pass for all playbooks
- Mutation testing confirms tests catch real issues

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix Python linting issue

- Remove unnecessary f-string prefix where no placeholders are used
- Fixes ruff F541 error

* Fix line length linting issues

- Break long lines to stay within 120 character limit
- Extract variables for better readability
- Fixes ruff E501 errors

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-15 09:54:45 -04:00

146 lines
4.8 KiB
Python

"""
Test to detect double Jinja2 templating issues in YAML files.
This test prevents Ansible 12+ errors from embedded templates in Jinja2 expressions.
The pattern `{{ lookup('file', '{{ var }}') }}` is invalid and must be
`{{ lookup('file', var) }}` instead.
Issue: https://github.com/trailofbits/algo/issues/14835
"""
import re
from pathlib import Path
import pytest
def find_yaml_files() -> list[Path]:
"""Find all YAML files in the repository."""
repo_root = Path(__file__).parent.parent.parent
yaml_files = []
# Include all .yml and .yaml files
for pattern in ["**/*.yml", "**/*.yaml"]:
yaml_files.extend(repo_root.glob(pattern))
# Exclude test files and vendor directories
excluded_dirs = {"venv", ".venv", "env", ".git", "__pycache__", ".pytest_cache"}
yaml_files = [
f for f in yaml_files
if not any(excluded in f.parts for excluded in excluded_dirs)
]
return sorted(yaml_files)
def detect_double_templating(content: str) -> list[tuple[int, str]]:
"""
Detect double templating patterns in file content.
Returns list of (line_number, problematic_line) tuples.
"""
issues = []
# Pattern 1: lookup() with embedded {{ }}
# Matches: lookup('file', '{{ var }}') or lookup("file", "{{ var }}")
pattern1 = r"lookup\s*\([^)]*['\"]{{[^}]*}}['\"][^)]*\)"
# Pattern 2: Direct nested {{ {{ }} }}
pattern2 = r"{{\s*[^}]*{{\s*[^}]*}}"
# Pattern 3: Embedded templates in quoted strings within Jinja2
# This catches cases like value: "{{ '{{ var }}' }}"
pattern3 = r"{{\s*['\"][^'\"]*{{[^}]*}}[^'\"]*['\"]"
lines = content.split('\n')
for i, line in enumerate(lines, 1):
# Skip comments
stripped = line.split('#')[0]
if not stripped.strip():
continue
if (re.search(pattern1, stripped) or
re.search(pattern2, stripped) or
re.search(pattern3, stripped)):
issues.append((i, line))
return issues
def test_no_double_templating():
"""Test that no YAML files contain double templating patterns."""
yaml_files = find_yaml_files()
all_issues = {}
for yaml_file in yaml_files:
try:
content = yaml_file.read_text()
issues = detect_double_templating(content)
if issues:
# Store relative path for cleaner output
rel_path = yaml_file.relative_to(Path(__file__).parent.parent.parent)
all_issues[str(rel_path)] = issues
except Exception:
# Skip binary files or files we can't read
continue
if all_issues:
# Format error message for clarity
error_msg = "\n\nDouble templating issues found:\n"
error_msg += "=" * 60 + "\n"
for file_path, issues in all_issues.items():
error_msg += f"\n{file_path}:\n"
for line_num, line in issues:
error_msg += f" Line {line_num}: {line.strip()}\n"
error_msg += "\n" + "=" * 60 + "\n"
error_msg += "Fix: Replace '{{ var }}' with var inside lookup() calls\n"
error_msg += "Example: lookup('file', '{{ SSH_keys.public }}') → lookup('file', SSH_keys.public)\n"
pytest.fail(error_msg)
def test_specific_known_issues():
"""
Test for specific known double-templating issues.
This ensures our detection catches the actual bugs from issue #14835.
"""
# These are the actual problematic patterns from the codebase
known_bad_patterns = [
"{{ lookup('file', '{{ SSH_keys.public }}') }}",
'{{ lookup("file", "{{ credentials_file_path }}") }}',
"value: \"{{ lookup('file', '{{ SSH_keys.public }}') }}\"",
"PayloadContentCA: \"{{ lookup('file' , '{{ ipsec_pki_path }}/cacert.pem')|b64encode }}\"",
]
for pattern in known_bad_patterns:
issues = detect_double_templating(pattern)
assert issues, f"Failed to detect known bad pattern: {pattern}"
def test_valid_patterns_not_flagged():
"""
Test that valid templating patterns are not flagged as errors.
"""
valid_patterns = [
"{{ lookup('file', SSH_keys.public) }}",
"{{ lookup('file', credentials_file_path) }}",
"value: \"{{ lookup('file', SSH_keys.public) }}\"",
"{{ item.1 }}.mobileconfig",
"{{ loop.index }}. {{ r.server }} ({{ r.IP_subject_alt_name }})",
"PayloadContentCA: \"{{ lookup('file', ipsec_pki_path + '/cacert.pem')|b64encode }}\"",
"ssh_pub_key: \"{{ lookup('file', SSH_keys.public) }}\"",
]
for pattern in valid_patterns:
issues = detect_double_templating(pattern)
assert not issues, f"Valid pattern incorrectly flagged: {pattern}"
if __name__ == "__main__":
# Run the test directly for debugging
test_specific_known_issues()
test_valid_patterns_not_flagged()
test_no_double_templating()
print("All tests passed!")