mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-03-12 21:23:05 -07:00
When the Hashview server returns HTTP 200 with an error message and no job_id (due to its internal notify_email bug), the CLI and interactive paths now: - exit 1 (not 0) in the CLI path - print "✗ Error" instead of "✓ Success" - print a hint to check the Hashview UI before retrying, preventing duplicate job creation Adds test for the error response path in test_cli_flags.py.
391 lines
13 KiB
Python
391 lines
13 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",
|
|
],
|
|
)
|
|
assert code == 0
|
|
out = capsys.readouterr().out
|
|
assert "Hashfile uploaded" in out
|
|
assert "Job created" in out
|
|
|
|
|
|
def test_hashview_upload_hashfile_job_no_notify_email_by_default(
|
|
monkeypatch, tmp_path, capsys
|
|
):
|
|
"""CLI must not send notify_email - it causes the server to create the job but
|
|
return 'Failed to add job: notify_email is invalid', hiding the created job and
|
|
causing a second job to be created on retry."""
|
|
captured_kwargs: dict = {}
|
|
|
|
class TrackingAPI:
|
|
def __init__(self, base_url, api_key, debug=False):
|
|
pass
|
|
|
|
def upload_hashfile(
|
|
self, file_path, customer_id, hash_type, file_format=5, hashfile_name=None
|
|
):
|
|
return {"msg": "Hashfile uploaded", "hashfile_id": 456}
|
|
|
|
def create_job(
|
|
self, name, hashfile_id, customer_id, limit_recovered=False, notify_email=None
|
|
):
|
|
captured_kwargs["notify_email"] = notify_email
|
|
return {"msg": "Job created", "job_id": 789}
|
|
|
|
hashfile = tmp_path / "hashes.txt"
|
|
hashfile.write_text("hash1\n")
|
|
monkeypatch.setattr(hc_main, "HashviewAPI", TrackingAPI)
|
|
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",
|
|
],
|
|
)
|
|
assert code == 0
|
|
assert captured_kwargs["notify_email"] is None
|
|
|
|
|
|
def test_hashview_upload_hashfile_job_error_response_exits_nonzero(
|
|
monkeypatch, tmp_path, capsys
|
|
):
|
|
"""When create_job returns an error response (no job_id), exit code must be 1
|
|
and output must show ✗ Error with a hint to check the Hashview UI."""
|
|
|
|
class ErrorJobAPI:
|
|
def __init__(self, base_url, api_key, debug=False):
|
|
pass
|
|
|
|
def upload_hashfile(
|
|
self, file_path, customer_id, hash_type, file_format=5, hashfile_name=None
|
|
):
|
|
return {"msg": "Hashfile uploaded", "hashfile_id": 456}
|
|
|
|
def create_job(
|
|
self, name, hashfile_id, customer_id, limit_recovered=False, notify_email=None
|
|
):
|
|
return {
|
|
"msg": "Failed to add job: 'notify_email' is an invalid keyword argument for JobNotifications"
|
|
}
|
|
|
|
hashfile = tmp_path / "hashes.txt"
|
|
hashfile.write_text("hash1\n")
|
|
monkeypatch.setattr(hc_main, "HashviewAPI", ErrorJobAPI)
|
|
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",
|
|
],
|
|
)
|
|
assert code == 1
|
|
out = capsys.readouterr().out
|
|
assert "✗ Error" in out
|
|
assert "Job ID:" not in out
|
|
assert "Check the Hashview UI before retrying" 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"
|