fix(wordlist-optimize): remove allow_multiple crash, redundant import, add worker tests

- Remove `allow_multiple=True` from wordlist_optimize handler to avoid
  AttributeError when the return value is a list rather than a str
- Remove redundant `import tempfile` inside wordlist_optimize worker body
  (tempfile is already imported at module level on line 29)
- Add TestWordlistOptimizeWorker with 6 tests covering: fast-path
  (empty outdir), merge-path (subtract+append), new-length copy,
  splitlen failure, subtract failure, and missing-input skip

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-05-04 21:20:53 -04:00
co-authored by Claude Sonnet 4.6
parent cf12c3264e
commit 0d3282654b
3 changed files with 170 additions and 3 deletions
-1
View File
@@ -1196,7 +1196,6 @@ def wordlist_optimize(ctx: Any) -> None:
"""Prompt for input wordlists and output directory, then optimize."""
raw = ctx.select_file_with_autocomplete(
"\n[*] Enter input wordlist paths",
allow_multiple=True,
base_dir=ctx.hcatWordlists,
).strip()
inputs = [p.strip() for p in raw.split(",") if p.strip()]
-1
View File
@@ -4013,7 +4013,6 @@ def wordlist_gate(infile: str, outfile: str, mod: int, offset: int) -> bool:
def wordlist_optimize(input_wordlists: list[str], outdir: str) -> bool:
"""Consolidate wordlists into per-length deduplicated files in outdir."""
import tempfile
os.makedirs(outdir, exist_ok=True)
for wl in input_wordlists:
if not os.path.isfile(wl):
+170 -1
View File
@@ -1,6 +1,7 @@
import os
import shutil
from pathlib import Path
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, call, patch
import pytest
@@ -386,3 +387,171 @@ class TestWordlistOptimize:
wordlist_optimize(ctx)
out = capsys.readouterr().out
assert "Optimization failed" in out
class TestWordlistOptimizeWorker:
"""Tests for the wordlist_optimize worker function in hate_crack.main.
All binary-calling helpers (wordlist_splitlen, wordlist_subtract_single)
are mocked so no real binaries are required.
"""
def _get_worker(self):
import importlib
import sys
# Import the module fresh; SKIP_INIT is already active via conftest.
mod = sys.modules.get("hate_crack.main")
if mod is None:
mod = importlib.import_module("hate_crack.main")
return mod.wordlist_optimize
# ------------------------------------------------------------------
# (a) fast-path: empty outdir → wordlist_splitlen called directly
# ------------------------------------------------------------------
def test_fast_path_empty_outdir(self, tmp_path):
worker = self._get_worker()
wl = tmp_path / "words.txt"
wl.write_text("word\n")
outdir = tmp_path / "out"
outdir.mkdir()
with patch("hate_crack.main.wordlist_splitlen", return_value=True) as mock_split, \
patch("hate_crack.main.wordlist_subtract_single") as mock_sub:
result = worker([str(wl)], str(outdir))
assert result is True
mock_split.assert_called_once_with(str(wl), str(outdir))
mock_sub.assert_not_called()
# ------------------------------------------------------------------
# (b) merge-path: existing per-length file → subtract_single + append
# ------------------------------------------------------------------
def test_merge_path_existing_length_file(self, tmp_path):
worker = self._get_worker()
wl_a = tmp_path / "a.txt"
wl_a.write_text("word1\n")
wl_b = tmp_path / "b.txt"
wl_b.write_text("word2\n")
outdir = tmp_path / "out"
outdir.mkdir()
# After the first wordlist, outdir contains "len5.txt"
len_file = outdir / "len5.txt"
len_file.write_text("word1\n")
# The second call goes through the merge path for the tmp subdir.
# wordlist_splitlen for wl_b produces "len5.txt" in a temp dir.
# wordlist_subtract_single produces a non-empty output → append.
new_words = b"word2\n"
def fake_splitlen(infile: str, outdir_arg: str) -> bool:
# Write a len5.txt into wherever we are called to write to
(Path(outdir_arg) / "len5.txt").write_bytes(b"word2\n")
return True
def fake_subtract(src: str, dst: str, out: str) -> bool:
# Simulate: the diff is "word2\n"
with open(out, "wb") as f:
f.write(new_words)
return True
with patch("hate_crack.main.wordlist_splitlen", side_effect=fake_splitlen), \
patch("hate_crack.main.wordlist_subtract_single", side_effect=fake_subtract):
# outdir is already non-empty (len_file exists), so wl_b goes to merge path
result = worker([str(wl_b)], str(outdir))
assert result is True
# len5.txt should now contain the appended new words
contents = len_file.read_bytes()
assert b"word2\n" in contents
# ------------------------------------------------------------------
# (c) new length file in subsequent wordlist is just copied
# ------------------------------------------------------------------
def test_new_length_file_is_copied(self, tmp_path):
worker = self._get_worker()
wl_b = tmp_path / "b.txt"
wl_b.write_text("verylongword\n")
outdir = tmp_path / "out"
outdir.mkdir()
# Seed outdir with one length file (len5) so it is non-empty
(outdir / "len5.txt").write_text("hello\n")
# The second wordlist produces only "len12.txt" (no clash)
def fake_splitlen(infile: str, outdir_arg: str) -> bool:
(Path(outdir_arg) / "len12.txt").write_bytes(b"verylongword\n")
return True
with patch("hate_crack.main.wordlist_splitlen", side_effect=fake_splitlen), \
patch("hate_crack.main.wordlist_subtract_single") as mock_sub:
result = worker([str(wl_b)], str(outdir))
assert result is True
assert (outdir / "len12.txt").exists()
assert (outdir / "len12.txt").read_bytes() == b"verylongword\n"
mock_sub.assert_not_called()
# ------------------------------------------------------------------
# (d) wordlist_splitlen failure returns False
# ------------------------------------------------------------------
def test_splitlen_failure_returns_false(self, tmp_path):
worker = self._get_worker()
wl = tmp_path / "words.txt"
wl.write_text("word\n")
outdir = tmp_path / "out"
outdir.mkdir()
with patch("hate_crack.main.wordlist_splitlen", return_value=False):
result = worker([str(wl)], str(outdir))
assert result is False
# ------------------------------------------------------------------
# (e) wordlist_subtract_single failure returns False
# ------------------------------------------------------------------
def test_subtract_failure_returns_false(self, tmp_path):
worker = self._get_worker()
wl_b = tmp_path / "b.txt"
wl_b.write_text("word2\n")
outdir = tmp_path / "out"
outdir.mkdir()
# Seed outdir so it's non-empty and has a clashing length file
(outdir / "len5.txt").write_text("word1\n")
def fake_splitlen(infile: str, outdir_arg: str) -> bool:
(Path(outdir_arg) / "len5.txt").write_bytes(b"word2\n")
return True
with patch("hate_crack.main.wordlist_splitlen", side_effect=fake_splitlen), \
patch("hate_crack.main.wordlist_subtract_single", return_value=False):
result = worker([str(wl_b)], str(outdir))
assert result is False
# ------------------------------------------------------------------
# (f) missing input file is skipped and processing continues
# ------------------------------------------------------------------
def test_missing_input_skipped_processing_continues(self, tmp_path, capsys):
worker = self._get_worker()
good_wl = tmp_path / "good.txt"
good_wl.write_text("word\n")
missing = str(tmp_path / "nonexistent.txt")
outdir = tmp_path / "out"
outdir.mkdir()
call_count = {"n": 0}
def fake_splitlen(infile: str, outdir_arg: str) -> bool:
call_count["n"] += 1
return True
with patch("hate_crack.main.wordlist_splitlen", side_effect=fake_splitlen):
result = worker([missing, str(good_wl)], str(outdir))
assert result is True
# Only the good wordlist should have been processed
assert call_count["n"] == 1
out = capsys.readouterr().out
assert "Skipping" in out