Public Access
DEVX-152: feat: sync missing features from v0.49.x line to master
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
"""Unit tests for devx.tools.check_ansible_patterns."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from devx.tools.check_ansible_patterns import (
|
||||
_check_file,
|
||||
_check_task,
|
||||
_check_tasks,
|
||||
_find_task_files,
|
||||
_is_legitimate_devnull,
|
||||
_is_legitimate_or_true,
|
||||
main,
|
||||
)
|
||||
|
||||
|
||||
def _make_task(name: str, action: str, value: str, **extra: object) -> dict:
|
||||
"""Build a minimal task dict for testing."""
|
||||
task: dict = {"name": name, action: value}
|
||||
task.update(extra)
|
||||
return task
|
||||
|
||||
|
||||
class TestIsLegitimateOrTrue:
|
||||
def test_cleanup_task_name_is_legitimate(self):
|
||||
assert _is_legitimate_or_true("docker rm old-container", "Remove old container")
|
||||
|
||||
def test_prune_task_name_is_legitimate(self):
|
||||
assert _is_legitimate_or_true("docker image prune -f", "Prune unused images")
|
||||
|
||||
def test_docker_rm_command_is_legitimate(self):
|
||||
assert _is_legitimate_or_true("docker rm -f mycontainer", "Some task")
|
||||
|
||||
def test_provision_task_is_not_legitimate(self):
|
||||
assert not _is_legitimate_or_true("curl -X POST https://api/app || true", "Provision OIDC client")
|
||||
|
||||
def test_sync_task_name_is_legitimate(self):
|
||||
assert _is_legitimate_or_true("psql -c 'ALTER USER' || true", "Sync PostgreSQL password")
|
||||
|
||||
|
||||
class TestCheckTask:
|
||||
def test_or_true_on_provision_task_fails(self, tmp_path: Path):
|
||||
task = _make_task(
|
||||
"Provision OIDC client",
|
||||
"ansible.builtin.shell",
|
||||
"curl -X POST https://zitadel/api || true",
|
||||
)
|
||||
violations = _check_task(task, tmp_path / "test.yml", 1, tmp_path)
|
||||
assert len(violations) >= 1
|
||||
assert "|| true" in violations[0]
|
||||
|
||||
def test_or_true_on_cleanup_task_passes(self, tmp_path: Path):
|
||||
task = _make_task(
|
||||
"Remove old container",
|
||||
"ansible.builtin.shell",
|
||||
"docker rm -f old-container || true",
|
||||
)
|
||||
assert _check_task(task, tmp_path / "test.yml", 1, tmp_path) == []
|
||||
|
||||
def test_failed_when_false_on_provision_fails(self, tmp_path: Path):
|
||||
task = _make_task(
|
||||
"Provision OIDC client",
|
||||
"ansible.builtin.shell",
|
||||
"curl -X POST https://zitadel/api",
|
||||
failed_when=False,
|
||||
)
|
||||
violations = _check_task(task, tmp_path / "test.yml", 1, tmp_path)
|
||||
assert any("failed_when" in v for v in violations)
|
||||
|
||||
def test_failed_when_false_on_stop_passes(self, tmp_path: Path):
|
||||
task = _make_task(
|
||||
"Stop ZITADEL containers",
|
||||
"ansible.builtin.shell",
|
||||
"docker stop zitadel",
|
||||
failed_when=False,
|
||||
)
|
||||
assert _check_task(task, tmp_path / "test.yml", 1, tmp_path) == []
|
||||
|
||||
def test_failed_when_false_on_check_passes(self, tmp_path: Path):
|
||||
task = _make_task(
|
||||
"Check if ZITADEL is running",
|
||||
"ansible.builtin.shell",
|
||||
"docker inspect zitadel",
|
||||
failed_when=False,
|
||||
)
|
||||
assert _check_task(task, tmp_path / "test.yml", 1, tmp_path) == []
|
||||
|
||||
def test_allow_marker_in_name_passes(self, tmp_path: Path):
|
||||
task = _make_task(
|
||||
"Provision OIDC #lint:allow-failure-masking",
|
||||
"ansible.builtin.shell",
|
||||
"curl -X POST https://zitadel/api || true",
|
||||
failed_when=False,
|
||||
)
|
||||
assert _check_task(task, tmp_path / "test.yml", 1, tmp_path) == []
|
||||
|
||||
def test_safe_task_no_violations(self, tmp_path: Path):
|
||||
task = _make_task(
|
||||
"Create directory",
|
||||
"ansible.builtin.file",
|
||||
"path=/opt/app state=directory",
|
||||
)
|
||||
assert _check_task(task, tmp_path / "test.yml", 1, tmp_path) == []
|
||||
|
||||
def test_relative_path_outside_repo(self, tmp_path: Path):
|
||||
"""Files outside repo_root use the full path in display."""
|
||||
task = _make_task(
|
||||
"Provision OIDC",
|
||||
"ansible.builtin.shell",
|
||||
"curl || true",
|
||||
)
|
||||
other_dir = Path("/tmp/other")
|
||||
violations = _check_task(task, other_dir / "test.yml", 1, tmp_path)
|
||||
assert len(violations) >= 1
|
||||
|
||||
|
||||
class TestCheckFile:
|
||||
def test_clean_file_passes(self, tmp_path: Path):
|
||||
p = tmp_path / "test.yml"
|
||||
p.write_text("- name: Safe task\n ansible.builtin.file:\n path: /opt/app\n state: directory\n")
|
||||
assert _check_file(p, tmp_path) == []
|
||||
|
||||
def test_dangerous_pattern_detected(self, tmp_path: Path):
|
||||
p = tmp_path / "test.yml"
|
||||
p.write_text(
|
||||
"- name: Provision OIDC\n"
|
||||
" ansible.builtin.shell: |\n"
|
||||
" curl -X POST https://api/app || true\n"
|
||||
" failed_when: false\n"
|
||||
)
|
||||
violations = _check_file(p, tmp_path)
|
||||
assert len(violations) >= 1
|
||||
|
||||
def test_file_level_allow_marker_passes(self, tmp_path: Path):
|
||||
p = tmp_path / "test.yml"
|
||||
p.write_text(
|
||||
"# lint:allow-failure-masking\n"
|
||||
"- name: Provision OIDC\n"
|
||||
" ansible.builtin.shell: |\n"
|
||||
" curl -X POST https://api/app || true\n"
|
||||
" failed_when: false\n"
|
||||
)
|
||||
assert _check_file(p, tmp_path) == []
|
||||
|
||||
def test_nonexistent_file_returns_empty(self):
|
||||
assert _check_file(Path("/nonexistent/path/file.yml"), Path.cwd()) == []
|
||||
|
||||
def test_yaml_parse_error_returns_empty(self, tmp_path: Path):
|
||||
p = tmp_path / "test.yml"
|
||||
p.write_text("name: Provision OIDC\n shell: curl || true\n: invalid: [")
|
||||
assert _check_file(p, tmp_path) == []
|
||||
|
||||
def test_dict_doc_playbook_with_tasks(self, tmp_path: Path):
|
||||
p = tmp_path / "playbook.yml"
|
||||
p.write_text(
|
||||
"- hosts: all\n"
|
||||
" tasks:\n"
|
||||
" - name: Provision OIDC\n"
|
||||
" ansible.builtin.shell: curl -X POST https://api/app || true\n"
|
||||
)
|
||||
violations = _check_file(p, tmp_path)
|
||||
assert any("|| true" in v for v in violations)
|
||||
|
||||
def test_dict_doc_with_pre_tasks_and_post_tasks(self, tmp_path: Path):
|
||||
p = tmp_path / "playbook.yml"
|
||||
p.write_text(
|
||||
"- hosts: all\n"
|
||||
" pre_tasks:\n"
|
||||
" - name: Provision secret\n"
|
||||
" ansible.builtin.shell: curl -X POST https://api/app || true\n"
|
||||
" post_tasks:\n"
|
||||
" - name: Provision OIDC\n"
|
||||
" ansible.builtin.shell: curl -X POST https://api/app || true\n"
|
||||
" handlers:\n"
|
||||
" - name: Provision password\n"
|
||||
" ansible.builtin.shell: curl -X POST https://api/app || true\n"
|
||||
)
|
||||
violations = _check_file(p, tmp_path)
|
||||
assert len(violations) >= 3
|
||||
|
||||
def test_block_tasks_in_list_item(self, tmp_path: Path):
|
||||
p = tmp_path / "tasks.yml"
|
||||
p.write_text(
|
||||
"- name: Outer task\n"
|
||||
" block:\n"
|
||||
" - name: Provision OIDC\n"
|
||||
" ansible.builtin.shell: curl -X POST https://api/app || true\n"
|
||||
" - name: Provision secret\n"
|
||||
" ansible.builtin.shell: curl -X POST https://api/app || true\n"
|
||||
)
|
||||
violations = _check_file(p, tmp_path)
|
||||
assert any("|| true" in v for v in violations)
|
||||
|
||||
def test_empty_doc_skipped(self, tmp_path: Path):
|
||||
p = tmp_path / "test.yml"
|
||||
p.write_text("---\nnull\n---\n- name: Provision OIDC\n ansible.builtin.shell: curl || true\n")
|
||||
violations = _check_file(p, tmp_path)
|
||||
assert any("|| true" in v for v in violations)
|
||||
|
||||
def test_pure_dict_doc_with_tasks(self, tmp_path: Path):
|
||||
p = tmp_path / "playbook.yml"
|
||||
p.write_text(
|
||||
"hosts: all\n"
|
||||
"tasks:\n"
|
||||
" - name: Provision OIDC\n"
|
||||
" ansible.builtin.shell: curl -X POST https://api/app || true\n"
|
||||
)
|
||||
violations = _check_file(p, tmp_path)
|
||||
assert any("|| true" in v for v in violations)
|
||||
|
||||
|
||||
class TestIsLegitimateDevnull:
|
||||
def test_cleanup_task_is_legitimate(self):
|
||||
assert _is_legitimate_devnull("docker rm old-container 2>/dev/null", "Remove old container")
|
||||
|
||||
def test_provision_task_is_not_legitimate(self):
|
||||
assert not _is_legitimate_devnull("curl -X POST https://api/app 2>/dev/null", "Provision OIDC client")
|
||||
|
||||
|
||||
class TestCheckTasks:
|
||||
def test_tasks_section_checked(self, tmp_path: Path):
|
||||
doc = {
|
||||
"tasks": [
|
||||
{"name": "Provision OIDC", "ansible.builtin.shell": "curl || true"},
|
||||
],
|
||||
}
|
||||
errors: list[str] = []
|
||||
_check_tasks(doc, tmp_path / "test.yml", errors, tmp_path)
|
||||
assert any("|| true" in e for e in errors)
|
||||
|
||||
def test_block_inside_tasks_section(self, tmp_path: Path):
|
||||
doc = {
|
||||
"tasks": [
|
||||
{
|
||||
"name": "Outer",
|
||||
"block": [
|
||||
{"name": "Provision secret", "ansible.builtin.shell": "curl || true"},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
errors: list[str] = []
|
||||
_check_tasks(doc, tmp_path / "test.yml", errors, tmp_path)
|
||||
assert any("|| true" in e for e in errors)
|
||||
|
||||
def test_non_list_section_ignored(self, tmp_path: Path):
|
||||
doc = {"tasks": "not a list"}
|
||||
errors: list[str] = []
|
||||
_check_tasks(doc, tmp_path / "test.yml", errors, tmp_path)
|
||||
assert errors == []
|
||||
|
||||
def test_non_dict_task_ignored(self, tmp_path: Path):
|
||||
doc = {"tasks": ["just a string"]}
|
||||
errors: list[str] = []
|
||||
_check_tasks(doc, tmp_path / "test.yml", errors, tmp_path)
|
||||
assert errors == []
|
||||
|
||||
|
||||
class TestFindTaskFiles:
|
||||
def test_single_file(self, tmp_path: Path):
|
||||
p = tmp_path / "main.yml"
|
||||
p.write_text("- name: test\n")
|
||||
assert _find_task_files(p) == [p]
|
||||
|
||||
def test_single_yaml_file(self, tmp_path: Path):
|
||||
p = tmp_path / "main.yaml"
|
||||
p.write_text("- name: test\n")
|
||||
assert _find_task_files(p) == [p]
|
||||
|
||||
def test_non_yaml_file_returns_empty(self, tmp_path: Path):
|
||||
p = tmp_path / "main.txt"
|
||||
p.write_text("hello\n")
|
||||
assert _find_task_files(p) == []
|
||||
|
||||
def test_directory_finds_yaml_files(self, tmp_path: Path):
|
||||
(tmp_path / "a.yml").write_text("- name: a\n")
|
||||
(tmp_path / "sub").mkdir()
|
||||
(tmp_path / "sub" / "b.yaml").write_text("- name: b\n")
|
||||
(tmp_path / "ignore.txt").write_text("nope\n")
|
||||
result = _find_task_files(tmp_path)
|
||||
names = {f.name for f in result}
|
||||
assert names == {"a.yml", "b.yaml"}
|
||||
|
||||
def test_directory_skips_molecule(self, tmp_path: Path):
|
||||
(tmp_path / "a.yml").write_text("- name: a\n")
|
||||
(tmp_path / "molecule").mkdir()
|
||||
(tmp_path / "molecule" / "scenario.yml").write_text("- name: mol\n")
|
||||
result = _find_task_files(tmp_path)
|
||||
assert all("molecule" not in f.parts for f in result)
|
||||
|
||||
def test_nonexistent_path_returns_empty(self):
|
||||
assert _find_task_files(Path("/nonexistent/path/xyz")) == []
|
||||
|
||||
|
||||
class TestMain:
|
||||
def test_main_clean_file_exit_zero(self, tmp_path: Path):
|
||||
p = tmp_path / "clean.yml"
|
||||
p.write_text("- name: Safe task\n ansible.builtin.file:\n path: /opt/app\n state: directory\n")
|
||||
result = CliRunner().invoke(main, ["--path", str(p)])
|
||||
assert result.exit_code == 0
|
||||
assert "OK" in result.output
|
||||
|
||||
def test_main_violation_exit_one(self, tmp_path: Path):
|
||||
p = tmp_path / "bad.yml"
|
||||
p.write_text(
|
||||
"- name: Provision OIDC\n"
|
||||
" ansible.builtin.shell: curl -X POST https://api/app || true\n"
|
||||
" failed_when: false\n"
|
||||
)
|
||||
result = CliRunner().invoke(main, ["--path", str(p)])
|
||||
assert result.exit_code == 1
|
||||
assert "FAIL" in result.output
|
||||
|
||||
def test_main_directory(self, tmp_path: Path):
|
||||
(tmp_path / "clean.yml").write_text(
|
||||
"- name: Safe task\n ansible.builtin.file:\n path: /opt\n state: directory\n"
|
||||
)
|
||||
result = CliRunner().invoke(main, ["--path", str(tmp_path)])
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_main_default_dirs(self, tmp_path: Path, monkeypatch):
|
||||
import devx.tools.check_ansible_patterns as mod
|
||||
|
||||
(tmp_path / "clean.yml").write_text(
|
||||
"- name: Safe task\n ansible.builtin.file:\n path: /opt\n state: directory\n"
|
||||
)
|
||||
monkeypatch.setattr(mod, "DEFAULT_ANSIBLE_DIRS", [tmp_path])
|
||||
result = CliRunner().invoke(main, [])
|
||||
assert result.exit_code == 0
|
||||
assert "OK" in result.output
|
||||
|
||||
def test_main_custom_ansible_dirs(self, tmp_path: Path):
|
||||
(tmp_path / "bad.yml").write_text(
|
||||
"- name: Provision OIDC\n ansible.builtin.shell: curl -X POST https://api/app || true\n"
|
||||
)
|
||||
result = CliRunner().invoke(main, ["--ansible-dir", str(tmp_path)])
|
||||
assert result.exit_code == 1
|
||||
Reference in New Issue
Block a user