Files
hate_crack/tests/test_wordlist_wrappers.py
Justin Bollinger c51ae332c6 feat: add wordlist tools submenu (len, req-include, req-exclude, cutb, rli, rli2, splitlen, gate) (#90, #91, #92, #94)
Add Wordlist Tools submenu (key 80) with 7 preprocessing utilities backed by hashcat-utils binaries:

- Filter by length (len.bin) - #90
- Require/exclude character classes (req-include.bin, req-exclude.bin) - #90
- Extract substring (cutb.bin) - #90
- Remove matching lines (rli.bin, rli2.bin) - #91
- Split by length (splitlen.bin) - #92
- Shard wordlist (gate.bin) - #94

Three-layer pattern:
- main.py: low-level wrappers (wordlist_filter_len, wordlist_filter_req_include,
  wordlist_filter_req_exclude, wordlist_cutb, wordlist_splitlen, wordlist_subtract,
  wordlist_subtract_single, wordlist_gate) return bool for testability
- attacks.py: UI handlers with input validation and the wordlist_tools_submenu dispatcher
- hate_crack.py + main.py: menu item "80" wired in both get_main_menu_items and
  get_main_menu_options

Move interactive_menu import to attacks.py module level (was local import in
combinator_submenu) to support patching in tests.
2026-03-19 12:15:43 -04:00

178 lines
6.5 KiB
Python

import os
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch, call
import importlib.util
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[1]
CLI_SPEC = importlib.util.spec_from_file_location(
"hate_crack_cli", PROJECT_ROOT / "hate_crack.py"
)
CLI_MODULE = importlib.util.module_from_spec(CLI_SPEC)
CLI_SPEC.loader.exec_module(CLI_MODULE)
def _get_main():
import hate_crack.main as m
return m
class TestWordlistFilterLenWrapper:
def test_calls_len_bin_with_correct_args(self, tmp_path):
m = _get_main()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
with patch("hate_crack.main.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
result = m.wordlist_filter_len(str(infile), str(outfile), 4, 8)
assert result is True
mock_run.assert_called_once()
cmd = mock_run.call_args[0][0]
assert cmd[1] == "4"
assert cmd[2] == "8"
assert "len.bin" in cmd[0]
def test_returns_false_on_nonzero_returncode(self, tmp_path):
m = _get_main()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
with patch("hate_crack.main.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=1)
result = m.wordlist_filter_len(str(infile), str(outfile), 4, 8)
assert result is False
class TestWordlistFilterReqIncludeWrapper:
def test_calls_req_include_bin(self, tmp_path):
m = _get_main()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
with patch("hate_crack.main.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
result = m.wordlist_filter_req_include(str(infile), str(outfile), 7)
assert result is True
cmd = mock_run.call_args[0][0]
assert "req-include.bin" in cmd[0]
assert cmd[1] == "7"
class TestWordlistFilterReqExcludeWrapper:
def test_calls_req_exclude_bin(self, tmp_path):
m = _get_main()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
with patch("hate_crack.main.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
result = m.wordlist_filter_req_exclude(str(infile), str(outfile), 8)
assert result is True
cmd = mock_run.call_args[0][0]
assert "req-exclude.bin" in cmd[0]
assert cmd[1] == "8"
class TestWordlistCutbWrapper:
def test_calls_cutb_bin_with_offset_and_length(self, tmp_path):
m = _get_main()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
with patch("hate_crack.main.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
result = m.wordlist_cutb(str(infile), str(outfile), 2, 4)
assert result is True
cmd = mock_run.call_args[0][0]
assert "cutb.bin" in cmd[0]
assert cmd[1] == "2"
assert cmd[2] == "4"
def test_calls_cutb_bin_without_length(self, tmp_path):
m = _get_main()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
with patch("hate_crack.main.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
result = m.wordlist_cutb(str(infile), str(outfile), 2, None)
assert result is True
cmd = mock_run.call_args[0][0]
assert "cutb.bin" in cmd[0]
assert cmd[1] == "2"
assert len(cmd) == 2 # only binary + offset, no length arg
class TestWordlistSplitlenWrapper:
def test_calls_splitlen_bin(self, tmp_path):
m = _get_main()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outdir = tmp_path / "split"
with patch("hate_crack.main.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
result = m.wordlist_splitlen(str(infile), str(outdir))
assert result is True
cmd = mock_run.call_args[0][0]
assert "splitlen.bin" in cmd[0]
assert cmd[1] == str(outdir)
class TestWordlistSubtractWrapper:
def test_calls_rli_bin_with_multiple_files(self, tmp_path):
m = _get_main()
infile = tmp_path / "in.txt"
infile.write_text("word1\n")
outfile = tmp_path / "out.txt"
remove1 = tmp_path / "r1.txt"
remove1.write_text("word1\n")
remove2 = tmp_path / "r2.txt"
remove2.write_text("word2\n")
with patch("hate_crack.main.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
result = m.wordlist_subtract(str(infile), str(outfile), str(remove1), str(remove2))
assert result is True
cmd = mock_run.call_args[0][0]
assert "rli.bin" in cmd[0]
assert cmd[1] == str(infile)
assert cmd[2] == str(outfile)
assert str(remove1) in cmd
assert str(remove2) in cmd
class TestWordlistSubtractSingleWrapper:
def test_calls_rli2_bin(self, tmp_path):
m = _get_main()
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"
with patch("hate_crack.main.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
result = m.wordlist_subtract_single(str(infile), str(removefile), str(outfile))
assert result is True
cmd = mock_run.call_args[0][0]
assert "rli2.bin" in cmd[0]
assert cmd[1] == str(infile)
assert cmd[2] == str(removefile)
class TestWordlistGateWrapper:
def test_calls_gate_bin(self, tmp_path):
m = _get_main()
infile = tmp_path / "in.txt"
infile.write_text("word1\nword2\nword3\n")
outfile = tmp_path / "shard.txt"
with patch("hate_crack.main.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
result = m.wordlist_gate(str(infile), str(outfile), 3, 0)
assert result is True
cmd = mock_run.call_args[0][0]
assert "gate.bin" in cmd[0]
assert cmd[1] == "3"
assert cmd[2] == "0"