diff options
Diffstat (limited to 'hooks/tests/test_destructive_bash_confirm.py')
| -rw-r--r-- | hooks/tests/test_destructive_bash_confirm.py | 137 |
1 files changed, 137 insertions, 0 deletions
diff --git a/hooks/tests/test_destructive_bash_confirm.py b/hooks/tests/test_destructive_bash_confirm.py new file mode 100644 index 0000000..50302e6 --- /dev/null +++ b/hooks/tests/test_destructive_bash_confirm.py @@ -0,0 +1,137 @@ +"""Tests for hooks/destructive-bash-confirm.py — shlex-based rm -rf parsing.""" + +from conftest import load_hook + +hook = load_hook("destructive-bash-confirm.py") + +SENTINEL = "(unparsed — shell too complex to inspect safely)" + + +# --- detection of flag forms (combined / separate / reordered) ------------- + +def test_rf_combined_detected(): + assert hook.detect_rm_rf("rm -rf build") == ["build"] + + +def test_r_f_separate_detected(): + assert hook.detect_rm_rf("rm -r -f build") == ["build"] + + +def test_fr_reordered_detected(): + assert hook.detect_rm_rf("rm -fr build") == ["build"] + + +def test_capital_R_detected(): + assert hook.detect_rm_rf("rm -Rf build") == ["build"] + + +def test_long_flags_detected(): + targets = hook.detect_rm_rf("rm --recursive --force build") + assert targets == ["build"] + + +# --- quoted / spaced paths now parse correctly ----------------------------- + +def test_quoted_path_with_space_parsed(): + assert hook.detect_rm_rf('rm -rf "my dir"') == ["my dir"] + + +def test_multiple_targets(): + assert hook.detect_rm_rf("rm -rf a b c") == ["a", "b", "c"] + + +def test_double_dash_separates_flags_from_paths(): + assert hook.detect_rm_rf("rm -rf -- -weird-name") == ["-weird-name"] + + +# --- not-a-match cases: no modal ------------------------------------------- + +def test_no_r_returns_none(): + assert hook.detect_rm_rf("rm -f file") is None + + +def test_no_f_returns_none(): + assert hook.detect_rm_rf("rm -r dir") is None + + +def test_not_rm_returns_none(): + assert hook.detect_rm_rf("rmdir foo") is None + + +def test_plain_rm_returns_none(): + assert hook.detect_rm_rf("rm file.txt") is None + + +# --- dangerous path banner still fires on parsed targets ------------------- + +def test_home_var_target_flags_dangerous(): + detection = hook.detect_destructive('rm -rf "$HOME/x"') + assert detection is not None + kind, ctx = detection + assert kind == "rm -rf" + assert "_banner" in ctx + assert "$HOME/x" in ctx["_banner"] + + +def test_root_path_flags_dangerous(): + detection = hook.detect_destructive("rm -rf /etc/foo") + assert detection is not None + _, ctx = detection + assert "_banner" in ctx + + +def test_safe_relative_target_no_banner(): + detection = hook.detect_destructive("rm -rf build/cache") + assert detection is not None + _, ctx = detection + assert "_banner" not in ctx + + +# --- fail-toward-asking on ambiguity --------------------------------------- + +def test_compound_command_returns_sentinel(): + # `ls && rm -rf foo` — the naive parser missed this; now we ask. + assert hook.detect_rm_rf("ls && rm -rf foo") == [SENTINEL] + + +def test_pipeline_returns_sentinel(): + assert hook.detect_rm_rf("find . -type d | xargs rm -rf") == [SENTINEL] + + +def test_semicolon_returns_sentinel(): + assert hook.detect_rm_rf("cd /tmp; rm -rf junk") == [SENTINEL] + + +def test_command_substitution_returns_sentinel(): + assert hook.detect_rm_rf("rm -rf $(echo target)") == [SENTINEL] + + +def test_backtick_substitution_returns_sentinel(): + assert hook.detect_rm_rf("rm -rf `echo target`") == [SENTINEL] + + +def test_redirect_returns_sentinel(): + assert hook.detect_rm_rf("rm -rf foo > /dev/null") == [SENTINEL] + + +def test_unbalanced_quotes_returns_sentinel(): + # shlex.split raises ValueError → ask anyway rather than silently pass. + assert hook.detect_rm_rf('rm -rf "unterminated') == [SENTINEL] + + +def test_compound_without_rm_rf_returns_none(): + # Compound construct but no dangerous rm — should not fire. + assert hook.detect_rm_rf("ls && echo done") is None + + +def test_compound_with_rm_but_no_force_returns_none(): + # `&&` present but the rm has no -f, so nothing to flag. + assert hook.detect_rm_rf("ls && rm -r dir") is None + + +def test_sentinel_fires_modal_via_detect_destructive(): + detection = hook.detect_destructive("ls && rm -rf foo") + assert detection is not None + kind, ctx = detection + assert kind == "rm -rf" + assert ctx["targets"] == [SENTINEL] |
