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"