Files
hate_crack/tests/test_wordlist_tools.py
Justin Bollinger 59f0052c0e feat: add tab autocomplete to wordlist menu file path prompts
Replace input() with ctx.select_file_with_autocomplete() for all file
and directory path prompts in the 7 wordlist tools submenu functions.
Non-path prompts (lengths, masks, offsets, mode selection) remain as
plain input() calls.

Update tests to set ctx.select_file_with_autocomplete.side_effect for
file path values and leave builtins.input patches only for non-path
inputs.
2026-03-20 10:30:08 -04:00

309 lines
13 KiB
Python

import os
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from hate_crack.attacks import (
wordlist_cut_substring,
wordlist_filter_charclass_exclude,
wordlist_filter_charclass_include,
wordlist_filter_length,
wordlist_shard,
wordlist_split_by_length,
wordlist_subtract_words,
wordlist_tools_submenu,
)
def _make_ctx():
ctx = MagicMock()
ctx.wordlist_filter_len.return_value = True
ctx.wordlist_filter_req_include.return_value = True
ctx.wordlist_filter_req_exclude.return_value = True
ctx.wordlist_cutb.return_value = True
ctx.wordlist_splitlen.return_value = True
ctx.wordlist_subtract.return_value = True
ctx.wordlist_subtract_single.return_value = True
ctx.wordlist_gate.return_value = True
return ctx
class TestWordlistFilterLength:
def test_calls_wordlist_filter_len(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["4", "8"]):
wordlist_filter_length(ctx)
ctx.wordlist_filter_len.assert_called_once_with(str(infile), str(outfile), 4, 8)
def test_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt"
wordlist_filter_length(ctx)
ctx.wordlist_filter_len.assert_not_called()
def test_rejects_empty_outfile(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
ctx.select_file_with_autocomplete.side_effect = [str(infile), ""]
wordlist_filter_length(ctx)
ctx.wordlist_filter_len.assert_not_called()
def test_prints_success_message(self, tmp_path, capsys):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["4", "8"]):
wordlist_filter_length(ctx)
out = capsys.readouterr().out
assert "success" in out.lower() or "wrote" in out.lower() or str(outfile) in out
def test_prints_failure_message(self, tmp_path, capsys):
ctx = _make_ctx()
ctx.wordlist_filter_len.return_value = False
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["4", "8"]):
wordlist_filter_length(ctx)
out = capsys.readouterr().out
assert "fail" in out.lower() or "error" in out.lower() or "!" in out
class TestWordlistFilterCharclassInclude:
def test_calls_wordlist_filter_req_include(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["7"]):
wordlist_filter_charclass_include(ctx)
ctx.wordlist_filter_req_include.assert_called_once_with(str(infile), str(outfile), 7)
def test_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt"
wordlist_filter_charclass_include(ctx)
ctx.wordlist_filter_req_include.assert_not_called()
class TestWordlistFilterCharclassExclude:
def test_calls_wordlist_filter_req_exclude(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["8"]):
wordlist_filter_charclass_exclude(ctx)
ctx.wordlist_filter_req_exclude.assert_called_once_with(str(infile), str(outfile), 8)
def test_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt"
wordlist_filter_charclass_exclude(ctx)
ctx.wordlist_filter_req_exclude.assert_not_called()
class TestWordlistCutSubstring:
def test_calls_wordlist_cutb_with_length(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["2", "4"]):
wordlist_cut_substring(ctx)
ctx.wordlist_cutb.assert_called_once_with(str(infile), str(outfile), 2, 4)
def test_calls_wordlist_cutb_without_length(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["2", ""]):
wordlist_cut_substring(ctx)
ctx.wordlist_cutb.assert_called_once_with(str(infile), str(outfile), 2, None)
def test_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt"
wordlist_cut_substring(ctx)
ctx.wordlist_cutb.assert_not_called()
class TestWordlistSplitByLength:
def test_calls_wordlist_splitlen(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outdir = tmp_path / "split"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outdir)]
wordlist_split_by_length(ctx)
ctx.wordlist_splitlen.assert_called_once_with(str(infile), str(outdir))
def test_creates_outdir_if_missing(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outdir = tmp_path / "split" / "nested"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outdir)]
wordlist_split_by_length(ctx)
assert outdir.exists()
def test_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt"
wordlist_split_by_length(ctx)
ctx.wordlist_splitlen.assert_not_called()
class TestWordlistSubtractWords:
def test_single_remove_calls_wordlist_subtract_single(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("word1\n")
removefile = tmp_path / "remove.txt"
removefile.write_text("word1\n")
outfile = tmp_path / "out.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(removefile), str(outfile)]
with patch("builtins.input", side_effect=["1"]):
wordlist_subtract_words(ctx)
ctx.wordlist_subtract_single.assert_called_once_with(str(infile), str(removefile), str(outfile))
def test_multi_remove_calls_wordlist_subtract(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("word1\nword2\n")
removefile1 = tmp_path / "remove1.txt"
removefile1.write_text("word1\n")
removefile2 = tmp_path / "remove2.txt"
removefile2.write_text("word2\n")
outfile = tmp_path / "out.txt"
ctx.select_file_with_autocomplete.side_effect = [
str(infile),
str(outfile),
f"{removefile1},{removefile2}",
]
with patch("builtins.input", side_effect=["2"]):
wordlist_subtract_words(ctx)
ctx.wordlist_subtract.assert_called_once_with(
str(infile), str(outfile), str(removefile1), str(removefile2)
)
def test_single_remove_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
ctx.select_file_with_autocomplete.return_value = "/nonexistent.txt"
with patch("builtins.input", side_effect=["1"]):
wordlist_subtract_words(ctx)
ctx.wordlist_subtract_single.assert_not_called()
class TestWordlistShard:
def test_calls_wordlist_gate_with_correct_args(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("word1\nword2\nword3\n")
outfile = tmp_path / "shard.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["3", "0"]):
wordlist_shard(ctx)
ctx.wordlist_gate.assert_called_once_with(str(infile), str(outfile), 3, 0)
def test_rejects_offset_gte_mod(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("word1\n")
outfile = tmp_path / "shard.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["3", "3"]):
wordlist_shard(ctx)
ctx.wordlist_gate.assert_not_called()
def test_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt"
wordlist_shard(ctx)
ctx.wordlist_gate.assert_not_called()
def test_rejects_mod_less_than_2(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("word1\n")
outfile = tmp_path / "shard.txt"
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["1", "0"]):
wordlist_shard(ctx)
ctx.wordlist_gate.assert_not_called()
class TestWordlistToolsSubmenu:
def test_submenu_dispatches_to_filter_length(self):
ctx = _make_ctx()
with patch("hate_crack.attacks.wordlist_filter_length") as mock_fn, \
patch("hate_crack.attacks.interactive_menu", side_effect=["1", "99"]):
wordlist_tools_submenu(ctx)
mock_fn.assert_called_once_with(ctx)
def test_submenu_dispatches_to_filter_charclass_include(self):
ctx = _make_ctx()
with patch("hate_crack.attacks.wordlist_filter_charclass_include") as mock_fn, \
patch("hate_crack.attacks.interactive_menu", side_effect=["2", "99"]):
wordlist_tools_submenu(ctx)
mock_fn.assert_called_once_with(ctx)
def test_submenu_dispatches_to_filter_charclass_exclude(self):
ctx = _make_ctx()
with patch("hate_crack.attacks.wordlist_filter_charclass_exclude") as mock_fn, \
patch("hate_crack.attacks.interactive_menu", side_effect=["3", "99"]):
wordlist_tools_submenu(ctx)
mock_fn.assert_called_once_with(ctx)
def test_submenu_dispatches_to_cut_substring(self):
ctx = _make_ctx()
with patch("hate_crack.attacks.wordlist_cut_substring") as mock_fn, \
patch("hate_crack.attacks.interactive_menu", side_effect=["4", "99"]):
wordlist_tools_submenu(ctx)
mock_fn.assert_called_once_with(ctx)
def test_submenu_dispatches_to_split_by_length(self):
ctx = _make_ctx()
with patch("hate_crack.attacks.wordlist_split_by_length") as mock_fn, \
patch("hate_crack.attacks.interactive_menu", side_effect=["5", "99"]):
wordlist_tools_submenu(ctx)
mock_fn.assert_called_once_with(ctx)
def test_submenu_dispatches_to_subtract_words(self):
ctx = _make_ctx()
with patch("hate_crack.attacks.wordlist_subtract_words") as mock_fn, \
patch("hate_crack.attacks.interactive_menu", side_effect=["6", "99"]):
wordlist_tools_submenu(ctx)
mock_fn.assert_called_once_with(ctx)
def test_submenu_dispatches_to_shard(self):
ctx = _make_ctx()
with patch("hate_crack.attacks.wordlist_shard") as mock_fn, \
patch("hate_crack.attacks.interactive_menu", side_effect=["7", "99"]):
wordlist_tools_submenu(ctx)
mock_fn.assert_called_once_with(ctx)
def test_submenu_exits_on_99(self):
ctx = _make_ctx()
with patch("hate_crack.attacks.interactive_menu", return_value="99"):
wordlist_tools_submenu(ctx)
def test_submenu_exits_on_none(self):
ctx = _make_ctx()
with patch("hate_crack.attacks.interactive_menu", return_value=None):
wordlist_tools_submenu(ctx)