Files
hate_crack/tests/test_hashcat_wrappers.py
Justin Bollinger 978a24a7c2 test: add 150 tests for attacks, wrappers, utils, api, and proxy
- tests/test_attacks_behavior.py: 29 tests for attack handler logic
  (loopback, extensive, top_mask, combinator, hybrid, ollama, simple pass-throughs)
- tests/test_hashcat_wrappers.py: 33 tests for hashcat subprocess wrappers
  (brute force, quick dict, combination, hybrid, prince, recycle, good measure, etc.)
- tests/test_main_utils.py: 44 tests for utility functions
  (_append_potfile_arg, generate_session_id, _ensure_hashfile_in_cwd,
  _run_hashcat_show, _dedup_netntlm_by_username, path resolution, cleanup)
- tests/test_api_downloads.py: 25 tests for api.py functions
  (sanitize_filename, check_7z, potfile config, hashmob key, extract_with_7z, download)
- tests/test_proxy.py: 18 tests for root module proxy mechanism
  (__getattr__, _sync_globals_to_main, _sync_callables_to_main, symbol re-export)

Also fix combinator_crack to abort gracefully when hcatCombinationWordlist is
a single string (only 1 wordlist configured) instead of crashing with IndexError.
2026-03-02 17:16:54 -05:00

625 lines
30 KiB
Python

from unittest.mock import MagicMock, patch
import pytest
@pytest.fixture
def main_module(hc_module):
"""Return the underlying hate_crack.main module for direct patching."""
return hc_module._main
def _make_mock_proc(wait_side_effect=None):
proc = MagicMock()
if wait_side_effect is not None:
proc.wait.side_effect = wait_side_effect
else:
proc.wait.return_value = None
proc.pid = 12345
return proc
def _common_patches(main_module, tmp_path):
"""Return a list of patch context managers for the most common globals."""
hash_file = str(tmp_path / "hashes.txt")
return [
patch.object(main_module, "hcatBin", "hashcat"),
patch.object(main_module, "hcatTuning", ""),
patch.object(main_module, "hcatPotfilePath", ""),
patch.object(main_module, "hcatHashFile", hash_file, create=True),
patch.object(main_module, "generate_session_id", return_value="test_session"),
]
class TestHcatBruteForce:
def test_contains_attack_mode_3(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
patch.object(main_module, "lineCount", return_value=0):
main_module.hcatBruteForce("1000", hash_file, 1, 7)
mock_popen.assert_called_once()
cmd = mock_popen.call_args[0][0]
assert "-a" in cmd
assert "3" in cmd
def test_increment_flags(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
patch.object(main_module, "lineCount", return_value=0):
main_module.hcatBruteForce("1000", hash_file, 3, 9)
cmd = mock_popen.call_args[0][0]
assert "--increment" in cmd
assert "--increment-min=3" in cmd
assert "--increment-max=9" in cmd
def test_keyboard_interrupt_kills_process(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt())
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc), \
patch.object(main_module, "lineCount", return_value=0):
main_module.hcatBruteForce("1000", hash_file, 1, 7)
mock_proc.kill.assert_called_once()
def test_hash_type_and_file_in_cmd(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
patch.object(main_module, "lineCount", return_value=0):
main_module.hcatBruteForce("500", hash_file, 1, 8)
cmd = mock_popen.call_args[0][0]
assert "-m" in cmd
assert "500" in cmd
assert hash_file in cmd
class TestHcatQuickDictionary:
def test_wordlist_added_to_cmd(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
wordlist = str(tmp_path / "words.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
patch("hate_crack.main._debug_cmd"):
main_module.hcatQuickDictionary("1000", hash_file, "", wordlist)
cmd = mock_popen.call_args[0][0]
assert wordlist in cmd
def test_loopback_flag_added(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
wordlist = str(tmp_path / "words.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
patch("hate_crack.main._debug_cmd"):
main_module.hcatQuickDictionary("1000", hash_file, "", wordlist, loopback=True)
cmd = mock_popen.call_args[0][0]
assert "--loopback" in cmd
def test_no_loopback_by_default(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
wordlist = str(tmp_path / "words.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
patch("hate_crack.main._debug_cmd"):
main_module.hcatQuickDictionary("1000", hash_file, "", wordlist)
cmd = mock_popen.call_args[0][0]
assert "--loopback" not in cmd
def test_chains_added_when_provided(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
wordlist = str(tmp_path / "words.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
patch("hate_crack.main._debug_cmd"):
main_module.hcatQuickDictionary(
"1000", hash_file, "-r /fake/rule.rule", wordlist
)
cmd = mock_popen.call_args[0][0]
assert "-r" in cmd
assert "/fake/rule.rule" in cmd
def test_list_of_wordlists_all_added(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
wl1 = str(tmp_path / "words1.txt")
wl2 = str(tmp_path / "words2.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
patch("hate_crack.main._debug_cmd"):
main_module.hcatQuickDictionary("1000", hash_file, "", [wl1, wl2])
cmd = mock_popen.call_args[0][0]
assert wl1 in cmd
assert wl2 in cmd
class TestHcatCombination:
def test_contains_attack_mode_1(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
wl1 = tmp_path / "words1.txt"
wl2 = tmp_path / "words2.txt"
wl1.write_text("word1\n")
wl2.write_text("word2\n")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatWordlists", str(tmp_path)), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
patch.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0, create=True):
main_module.hcatCombination(
"1000", hash_file, wordlists=[str(wl1), str(wl2)]
)
mock_popen.assert_called_once()
cmd = mock_popen.call_args[0][0]
assert "-a" in cmd
assert "1" in cmd
def test_aborts_with_fewer_than_two_wordlists(self, main_module, tmp_path, capsys):
hash_file = str(tmp_path / "hashes.txt")
wl1 = tmp_path / "words1.txt"
wl1.write_text("word1\n")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatWordlists", str(tmp_path)), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
main_module.hcatCombination("1000", hash_file, wordlists=[str(wl1)])
mock_popen.assert_not_called()
captured = capsys.readouterr()
assert "requires at least 2" in captured.out
class TestHcatHybrid:
def test_contains_attack_mode_6_or_7(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
wl = tmp_path / "words.txt"
wl.write_text("word\n")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatWordlists", str(tmp_path)), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
patch.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0, create=True):
main_module.hcatHybrid("1000", hash_file, wordlists=[str(wl)])
assert mock_popen.call_count >= 1
all_cmds = [c[0][0] for c in mock_popen.call_args_list]
modes_used = set()
for cmd in all_cmds:
if "-a" in cmd:
idx = cmd.index("-a")
modes_used.add(cmd[idx + 1])
assert modes_used & {"6", "7"}, f"Expected mode 6 or 7 in cmds, got modes: {modes_used}"
def test_aborts_when_no_valid_wordlists(self, main_module, tmp_path, capsys):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatWordlists", str(tmp_path)), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
main_module.hcatHybrid(
"1000", hash_file, wordlists=["/nonexistent/wordlist.txt"]
)
mock_popen.assert_not_called()
captured = capsys.readouterr()
assert "No valid wordlists" in captured.out
class TestHcatPathwellBruteForce:
def test_popen_called(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hate_path", str(tmp_path)), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
main_module.hcatPathwellBruteForce("1000", hash_file)
mock_popen.assert_called_once()
def test_attack_mode_3_in_cmd(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hate_path", str(tmp_path)), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
main_module.hcatPathwellBruteForce("1000", hash_file)
cmd = mock_popen.call_args[0][0]
assert "-a" in cmd
assert "3" in cmd
def test_pathwell_mask_in_cmd(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hate_path", str(tmp_path)), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
main_module.hcatPathwellBruteForce("1000", hash_file)
cmd = mock_popen.call_args[0][0]
assert any("pathwell.hcmask" in arg for arg in cmd)
class TestHcatPrince:
def test_two_popen_calls_for_pipe(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
prince_base = tmp_path / "prince_base.txt"
prince_base.write_text("password\n")
prince_dir = tmp_path / "princeprocessor"
prince_dir.mkdir()
(prince_dir / "pp64.bin").touch()
mock_prince_proc = MagicMock()
mock_prince_proc.stdout = MagicMock()
mock_prince_proc.wait.return_value = None
mock_hashcat_proc = MagicMock()
mock_hashcat_proc.wait.return_value = None
mock_hashcat_proc.pid = 12345
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hate_path", str(tmp_path)), \
patch.object(main_module, "hcatPrinceBin", "pp64.bin"), \
patch.object(main_module, "hcatPrinceBaseList", [str(prince_base)]), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch.object(main_module, "get_rule_path", return_value="/fake/prince_optimized.rule"), \
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
patch("hate_crack.main.subprocess.Popen") as mock_popen:
mock_popen.side_effect = [mock_prince_proc, mock_hashcat_proc]
main_module.hcatPrince("1000", hash_file)
assert mock_popen.call_count == 2
def test_prince_binary_first_call(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
prince_base = tmp_path / "prince_base.txt"
prince_base.write_text("password\n")
prince_dir = tmp_path / "princeprocessor"
prince_dir.mkdir()
pp_bin = prince_dir / "pp64.bin"
pp_bin.touch()
mock_prince_proc = MagicMock()
mock_prince_proc.stdout = MagicMock()
mock_prince_proc.wait.return_value = None
mock_hashcat_proc = MagicMock()
mock_hashcat_proc.wait.return_value = None
mock_hashcat_proc.pid = 12345
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hate_path", str(tmp_path)), \
patch.object(main_module, "hcatPrinceBin", "pp64.bin"), \
patch.object(main_module, "hcatPrinceBaseList", [str(prince_base)]), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch.object(main_module, "get_rule_path", return_value="/fake/prince_optimized.rule"), \
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
patch("hate_crack.main.subprocess.Popen") as mock_popen:
mock_popen.side_effect = [mock_prince_proc, mock_hashcat_proc]
main_module.hcatPrince("1000", hash_file)
prince_cmd = mock_popen.call_args_list[0][0][0]
assert str(pp_bin) in prince_cmd
def test_aborts_when_prince_base_missing(self, main_module, tmp_path, capsys):
hash_file = str(tmp_path / "hashes.txt")
with patch.object(main_module, "hate_path", str(tmp_path)), \
patch.object(main_module, "hcatPrinceBin", "pp64.bin"), \
patch.object(main_module, "hcatPrinceBaseList", ["/nonexistent/base.txt"]), \
patch.object(main_module, "get_rule_path", return_value="/fake/rule.rule"), \
patch("hate_crack.main.subprocess.Popen") as mock_popen:
main_module.hcatPrince("1000", hash_file)
mock_popen.assert_not_called()
captured = capsys.readouterr()
assert "not found" in captured.out
class TestHcatRecycle:
def test_skipped_when_count_zero(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
with patch("hate_crack.main.subprocess.Popen") as mock_popen:
main_module.hcatRecycle("1000", hash_file, 0)
mock_popen.assert_not_called()
def test_popen_called_when_count_nonzero(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
out_file = tmp_path / "hashes.txt.out"
out_file.write_text("hash1:password1\nhash2:password2\n")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatRules", ["best66.rule"]), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch.object(main_module, "get_rule_path", return_value="/fake/best66.rule"), \
patch("hate_crack.main._write_delimited_field"), \
patch("hate_crack.main.convert_hex", return_value=["password1", "password2"]), \
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
patch("builtins.open", create=True) as mock_open, \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
mock_open.return_value.__enter__ = MagicMock(return_value=MagicMock())
mock_open.return_value.__exit__ = MagicMock(return_value=False)
main_module.hcatRecycle("1000", hash_file, 5)
assert mock_popen.call_count >= 1
def test_rule_path_in_cmd_when_count_nonzero(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatRules", ["best66.rule"]), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch.object(main_module, "get_rule_path", return_value="/fake/best66.rule"), \
patch("hate_crack.main._write_delimited_field"), \
patch("hate_crack.main.convert_hex", return_value=["pass1"]), \
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
patch("builtins.open", create=True) as mock_open, \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
mock_open.return_value.__enter__ = MagicMock(return_value=MagicMock())
mock_open.return_value.__exit__ = MagicMock(return_value=False)
main_module.hcatRecycle("1000", hash_file, 3)
cmd = mock_popen.call_args[0][0]
assert "-r" in cmd
assert "/fake/best66.rule" in cmd
class TestHcatGoodMeasure:
def test_popen_called_at_least_once(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatGoodMeasureBaseList", "/fake/base.txt"), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch.object(main_module, "get_rule_path", return_value="/fake/rule.rule"), \
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
patch.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0, create=True):
main_module.hcatGoodMeasure("1000", hash_file)
assert mock_popen.call_count >= 1
def test_hash_type_in_cmd(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatGoodMeasureBaseList", "/fake/base.txt"), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch.object(main_module, "get_rule_path", return_value="/fake/rule.rule"), \
patch("hate_crack.main._add_debug_mode_for_rules", side_effect=lambda c: c), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen, \
patch.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0, create=True):
main_module.hcatGoodMeasure("500", hash_file)
cmd = mock_popen.call_args_list[0][0][0]
assert "-m" in cmd
assert "500" in cmd
class TestHcatMiddleCombinator:
def test_popen_called_at_least_once(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatMiddleCombinatorMasks", ["!", "1"]), \
patch.object(main_module, "hcatMiddleBaseList", "/fake/base.txt"), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
main_module.hcatMiddleCombinator("1000", hash_file)
assert mock_popen.call_count >= 1
def test_attack_mode_1_in_cmd(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatMiddleCombinatorMasks", ["!"]), \
patch.object(main_module, "hcatMiddleBaseList", "/fake/base.txt"), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
main_module.hcatMiddleCombinator("1000", hash_file)
cmd = mock_popen.call_args_list[0][0][0]
assert "-a" in cmd
assert "1" in cmd
class TestHcatThoroughCombinator:
def test_popen_called_at_least_once(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatThoroughCombinatorMasks", ["!"]), \
patch.object(main_module, "hcatThoroughBaseList", "/fake/base.txt"), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
main_module.hcatThoroughCombinator("1000", hash_file)
assert mock_popen.call_count >= 1
class TestHcatYoloCombination:
def test_popen_called_once_then_keyboard_interrupt(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
wl_dir = tmp_path / "wordlists"
wl_dir.mkdir()
(wl_dir / "words.txt").write_text("word\n")
mock_proc = MagicMock()
mock_proc.pid = 12345
mock_proc.wait.side_effect = KeyboardInterrupt()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatWordlists", str(wl_dir)), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
main_module.hcatYoloCombination("1000", hash_file)
assert mock_popen.call_count >= 1
mock_proc.kill.assert_called()
def test_attack_mode_1_in_cmd(self, main_module, tmp_path):
hash_file = str(tmp_path / "hashes.txt")
wl_dir = tmp_path / "wordlists"
wl_dir.mkdir()
(wl_dir / "words.txt").write_text("word\n")
mock_proc = MagicMock()
mock_proc.pid = 12345
mock_proc.wait.side_effect = KeyboardInterrupt()
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "hcatWordlists", str(wl_dir)), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
main_module.hcatYoloCombination("1000", hash_file)
cmd = mock_popen.call_args_list[0][0][0]
assert "-a" in cmd
assert "1" in cmd
class TestAppendPotfileArg:
def test_appends_when_potfile_set(self, main_module):
cmd = ["hashcat", "-m", "1000"]
with patch.object(main_module, "hcatPotfilePath", "/fake/hashcat.pot"):
main_module._append_potfile_arg(cmd)
assert "--potfile-path=/fake/hashcat.pot" in cmd
def test_no_append_when_empty(self, main_module):
cmd = ["hashcat", "-m", "1000"]
with patch.object(main_module, "hcatPotfilePath", ""):
main_module._append_potfile_arg(cmd)
assert not any(arg.startswith("--potfile-path") for arg in cmd)
def test_no_append_when_use_potfile_false(self, main_module):
cmd = ["hashcat", "-m", "1000"]
with patch.object(main_module, "hcatPotfilePath", "/fake/hashcat.pot"):
main_module._append_potfile_arg(cmd, use_potfile_path=False)
assert not any(arg.startswith("--potfile-path") for arg in cmd)
def test_explicit_potfile_path_overrides_global(self, main_module):
cmd = ["hashcat", "-m", "1000"]
with patch.object(main_module, "hcatPotfilePath", "/global/hashcat.pot"):
main_module._append_potfile_arg(cmd, potfile_path="/override/custom.pot")
assert "--potfile-path=/override/custom.pot" in cmd
assert "--potfile-path=/global/hashcat.pot" not in cmd