From 5b0c119ec0057a69fb5c6c6ebdc19196fddef7f4 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Mon, 9 Mar 2026 13:17:50 -0400 Subject: [PATCH] fix: handle Hashview create_job error response correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- hate_crack/main.py | 31 ++++++++----- tests/test_cli_flags.py | 97 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 12 deletions(-) diff --git a/hate_crack/main.py b/hate_crack/main.py index 993883e..09816a3 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -2898,10 +2898,9 @@ def hashview_api(): customer_id, limit_recovered, ) - print( - f"\n✓ Success: {job_result.get('msg', 'Job created')}" - ) + msg = job_result.get("msg", "") if "job_id" in job_result: + print(f"\n✓ Success: {msg or 'Job created'}") print(f" Job ID: {job_result['job_id']}") print( "\nNote: Job created with automatically assigned tasks based on" @@ -2928,6 +2927,14 @@ def hashview_api(): print( f"\n✓ Success: {start_result.get('msg', 'Job started')}" ) + else: + print( + f"\n✗ Error: {msg or 'Job creation failed (no job_id returned)'}" + ) + print( + " Note: The Hashview server may have created the job" + " despite this error. Check the Hashview UI before retrying." + ) except Exception as e: print(f"\n✗ Error creating job: {str(e)}") except Exception as e: @@ -3595,11 +3602,6 @@ def main(): action="store_true", help="Limit to recovered hashes only", ) - hv_upload_hashfile_job.add_argument( - "--no-notify-email", - action="store_true", - help="Disable email notifications", - ) return parser, hashview_parser # Removed add_common_args(parser) since config items are now only set via config file @@ -3761,12 +3763,19 @@ def main(): upload_result["hashfile_id"], args.customer_id, limit_recovered=args.limit_recovered, - notify_email=not args.no_notify_email, ) - print(f"\n✓ Success: {job_result.get('msg', 'Job created')}") + msg = job_result.get("msg", "") if "job_id" in job_result: + print(f"\n✓ Success: {msg or 'Job created'}") print(f" Job ID: {job_result['job_id']}") - sys.exit(0) + sys.exit(0) + else: + print(f"\n✗ Error: {msg or 'Job creation failed (no job_id returned)'}") + print( + " Note: The Hashview server may have created the job despite this error." + " Check the Hashview UI before retrying." + ) + sys.exit(1) print("✗ Error: No hashview subcommand provided.") hashview_parser.print_help() diff --git a/tests/test_cli_flags.py b/tests/test_cli_flags.py index 2d2a693..67c9077 100644 --- a/tests/test_cli_flags.py +++ b/tests/test_cli_flags.py @@ -258,7 +258,6 @@ def test_hashview_upload_hashfile_job_flags(monkeypatch, tmp_path, capsys): "--job-name", "TestJob", "--limit-recovered", - "--no-notify-email", ], ) assert code == 0 @@ -267,6 +266,102 @@ def test_hashview_upload_hashfile_job_flags(monkeypatch, tmp_path, capsys): 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) # ---------------------------------------------------------------------------