Files
hate_crack/tests/test_cli_flags.py
T
Justin Bollinger 63c3ab93d2 test: add 23 unit tests for all CLI flags and argparse errors
Cover --weakpass, --hashmob, --rules, --cleanup, --download-torrent,
--download-all-torrents, --hashview, --download-hashview, --rank,
--potfile-path, --no-potfile-path, --debug, positional hashfile/hashtype
args, hashview download-hashes subcommand, upload-hashfile-job with
--limit-recovered/--no-notify-email, and argparse error cases.
2026-03-02 21:23:28 -05:00

296 lines
10 KiB
Python

import sys
import pytest
import hate_crack.main as hc_main
def _run_main(monkeypatch, argv):
"""Set sys.argv and run hc_main.main(), returning the exit code."""
monkeypatch.setattr(sys, "argv", ["hate_crack.py"] + argv)
with pytest.raises(SystemExit) as exc:
hc_main.main()
return exc.value.code
# ---------------------------------------------------------------------------
# 1. Top-level flags that dispatch to a handler and exit 0
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"argv, handler_attr",
[
(["--weakpass"], "weakpass_wordlist_menu"),
(["--hashmob"], "download_hashmob_wordlists"),
(["--rules"], "download_hashmob_rules"),
(["--cleanup"], "cleanup_wordlist_artifacts"),
],
)
def test_flag_dispatches_to_handler(monkeypatch, capsys, argv, handler_attr):
called = []
monkeypatch.setattr(
hc_main,
handler_attr,
lambda **kw: called.append(kw) or None,
)
code = _run_main(monkeypatch, argv)
assert code == 0
assert len(called) == 1
def test_download_torrent_flag(monkeypatch, capsys):
called = []
monkeypatch.setattr(
hc_main,
"download_weakpass_torrent",
lambda download_torrent, filename, print_fn=print: called.append(filename),
)
code = _run_main(monkeypatch, ["--download-torrent", "somefile.txt"])
assert code == 0
assert called == ["somefile.txt"]
def test_download_all_torrents_flag(monkeypatch):
called = []
monkeypatch.setattr(
hc_main,
"download_all_weakpass_torrents",
lambda fetch_all_wordlists, download_torrent, print_fn=print: called.append(
True
),
)
code = _run_main(monkeypatch, ["--download-all-torrents"])
assert code == 0
assert called == [True]
def test_hashview_flag(monkeypatch):
monkeypatch.setattr(hc_main, "hashview_api_key", "dummy")
called = []
monkeypatch.setattr(hc_main, "hashview_api", lambda: called.append(True))
code = _run_main(monkeypatch, ["--hashview"])
assert code == 0
assert called == [True]
def test_download_hashview_flag(monkeypatch):
"""--download-hashview falls into the menu loop, triggers hashview_api, then exits."""
monkeypatch.setattr(hc_main, "hashview_api_key", "dummy")
called = []
monkeypatch.setattr(hc_main, "hashview_api", lambda: called.append(True))
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
monkeypatch.setattr("builtins.input", lambda _prompt="": "1")
code = _run_main(monkeypatch, ["--download-hashview"])
assert code == 0
assert called == [True]
# ---------------------------------------------------------------------------
# 2. --weakpass --rank passthrough
# ---------------------------------------------------------------------------
def test_weakpass_rank_passthrough(monkeypatch):
called = []
monkeypatch.setattr(
hc_main,
"weakpass_wordlist_menu",
lambda **kw: called.append(kw),
)
code = _run_main(monkeypatch, ["--weakpass", "--rank", "5"])
assert code == 0
assert called == [{"rank": 5}]
def test_weakpass_default_rank(monkeypatch):
called = []
monkeypatch.setattr(
hc_main,
"weakpass_wordlist_menu",
lambda **kw: called.append(kw),
)
code = _run_main(monkeypatch, ["--weakpass"])
assert code == 0
assert called == [{"rank": -1}]
# ---------------------------------------------------------------------------
# 3. --potfile-path and --no-potfile-path
# ---------------------------------------------------------------------------
def test_potfile_path_flag(monkeypatch):
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
_run_main(monkeypatch, ["--potfile-path", "/tmp/test.pot"])
assert hc_main.hcatPotfilePath == "/tmp/test.pot"
def test_no_potfile_path_flag(monkeypatch):
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
_run_main(monkeypatch, ["--no-potfile-path"])
assert hc_main.hcatPotfilePath == ""
def test_potfile_path_empty_string_reverts_to_default(monkeypatch):
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
_run_main(monkeypatch, ["--potfile-path", ""])
assert hc_main.hcatPotfilePath == ""
# ---------------------------------------------------------------------------
# 4. --debug flag
# ---------------------------------------------------------------------------
def test_debug_flag(monkeypatch):
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
_run_main(monkeypatch, ["--debug"])
assert hc_main.debug_mode is True
# ---------------------------------------------------------------------------
# 5. Positional arguments: hashfile and hashtype
# ---------------------------------------------------------------------------
def test_positional_hashfile_and_hashtype(monkeypatch, tmp_path):
hashfile = tmp_path / "hashes.txt"
hashfile.write_text("aabbccdd\n")
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
# Mock the main menu loop to prevent interactive prompts after globals are set
monkeypatch.setattr(
hc_main, "get_main_menu_options", lambda: {"q": ("Quit", lambda: sys.exit(0))}
)
monkeypatch.setattr("builtins.input", lambda _prompt="": "q")
_run_main(monkeypatch, [str(hashfile), "1000"])
assert hc_main.hcatHashType == "1000"
assert hc_main.hcatHashFile is not None
def test_positional_hashfile_only_enters_menu(monkeypatch, tmp_path):
"""With only hashfile (no hashtype), falls through to the interactive menu."""
hashfile = tmp_path / "hashes.txt"
hashfile.write_text("aabbccdd\n")
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
code = _run_main(monkeypatch, [str(hashfile)])
assert code == 0
def test_no_args_enters_menu(monkeypatch):
"""No arguments falls through to the interactive menu."""
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
code = _run_main(monkeypatch, [])
assert code == 0
# ---------------------------------------------------------------------------
# 6. Hashview subcommand: download-hashes
# ---------------------------------------------------------------------------
class DummyHashviewAPI:
def __init__(self, base_url, api_key, debug=False):
self.calls = []
def download_left_hashes(self, customer_id, hashfile_id, hash_type=None):
self.calls.append(
("download_left_hashes", customer_id, hashfile_id, hash_type)
)
return {"output_file": "left.txt", "size": 42}
def test_hashview_download_hashes(monkeypatch, capsys):
monkeypatch.setattr(hc_main, "HashviewAPI", DummyHashviewAPI)
monkeypatch.setattr(hc_main, "hashview_api_key", "dummy")
monkeypatch.setattr(hc_main, "hashview_url", "https://hv.example.com")
code = _run_main(
monkeypatch,
[
"hashview",
"download-hashes",
"--customer-id",
"1",
"--hashfile-id",
"2",
"--hash-type",
"1000",
],
)
assert code == 0
out = capsys.readouterr().out
assert "42 bytes" in out
# ---------------------------------------------------------------------------
# 7. Hashview upload-hashfile-job with --limit-recovered and --no-notify-email
# ---------------------------------------------------------------------------
class DummyHashviewAPIFull:
def __init__(self, base_url, api_key, debug=False):
self.calls = []
def upload_hashfile(
self, file_path, customer_id, hash_type, file_format=5, hashfile_name=None
):
self.calls.append(("upload_hashfile", file_path))
return {"msg": "Hashfile uploaded", "hashfile_id": 456}
def create_job(
self, name, hashfile_id, customer_id, limit_recovered=False, notify_email=True
):
self.calls.append(
("create_job", limit_recovered, notify_email)
)
return {"msg": "Job created", "job_id": 789}
def test_hashview_upload_hashfile_job_flags(monkeypatch, tmp_path, capsys):
hashfile = tmp_path / "hashes.txt"
hashfile.write_text("hash1\n")
monkeypatch.setattr(hc_main, "HashviewAPI", DummyHashviewAPIFull)
monkeypatch.setattr(hc_main, "hashview_api_key", "dummy")
monkeypatch.setattr(hc_main, "hashview_url", "https://hv.example.com")
code = _run_main(
monkeypatch,
[
"hashview",
"upload-hashfile-job",
"--file",
str(hashfile),
"--customer-id",
"1",
"--hash-type",
"1000",
"--job-name",
"TestJob",
"--limit-recovered",
"--no-notify-email",
],
)
assert code == 0
out = capsys.readouterr().out
assert "Hashfile uploaded" in out
assert "Job created" in out
# ---------------------------------------------------------------------------
# 8. Argparse error cases (exit 2)
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"argv",
[
["--download-torrent"], # missing filename
["hashview", "upload-cracked"], # missing --file
["--potfile-path"], # missing path argument
],
)
def test_argparse_missing_required_args(monkeypatch, argv):
monkeypatch.setattr(hc_main, "hashview_api_key", "dummy")
monkeypatch.setattr(hc_main, "hashview_url", "https://hv.example.com")
code = _run_main(monkeypatch, argv)
assert code == 2
def test_potfile_path_and_no_potfile_path_conflict(monkeypatch):
"""Both --potfile-path and --no-potfile-path should still parse (not mutually exclusive in argparse)."""
monkeypatch.setattr(hc_main, "ascii_art", lambda: None)
monkeypatch.setattr("builtins.input", lambda _prompt="": "5")
# --potfile-path wins because it's checked second in the dispatch logic
code = _run_main(monkeypatch, ["--potfile-path", "/tmp/test.pot", "--no-potfile-path"])
assert code == 0
assert hc_main.hcatPotfilePath == "/tmp/test.pot"