Merge branch 'feat/test-pushover-menu' into feat/pushover-notifications

Adds menu option 84 (Send Test Pushover Notification) so users can verify
their Pushover credentials without running an attack. Ignores the global
notify_enabled toggle by design (prints a note when OFF).
This commit is contained in:
Justin Bollinger
2026-04-22 18:29:05 -04:00
4 changed files with 116 additions and 0 deletions
+1
View File
@@ -95,6 +95,7 @@ def get_main_menu_options():
"80": _attacks.wordlist_tools_submenu,
"81": _attacks.rule_tools_submenu,
"83": toggle_notifications,
"84": test_pushover_notification,
"90": download_hashmob_rules,
"91": weakpass_wordlist_menu,
"92": download_hashmob_wordlists,
+35
View File
@@ -4108,6 +4108,39 @@ def toggle_notifications():
)
def test_pushover_notification():
"""Send a canned test notification so the user can verify Pushover works.
Ignores the global ``notify_enabled`` toggle on purpose: the point of the
test is to confirm the wire is live, independent of whether attacks are
currently wired to notify. When the global toggle is OFF we still send
but print a note so the user is not surprised later.
"""
settings = _notify.get_settings()
token = settings.pushover_token
user = settings.pushover_user
if not token or not user:
print(
"\n[!] Pushover credentials missing. Set notify_pushover_token "
"and notify_pushover_user in config.json."
)
return
if not settings.enabled:
print("\n(notifications are globally OFF, but sending test anyway)")
title = "hate_crack: test notification"
message = (
"This is a test notification from hate_crack. "
"If you see this, Pushover is wired up correctly."
)
ok = _notify._send_pushover(token, user, title, message)
if ok:
print("[+] Test Pushover notification sent. Check your device.")
else:
print("[!] Test Pushover notification failed. See log output for details.")
def get_main_menu_items():
"""Return ordered (key, label) pairs for the main menu."""
items = [
@@ -4136,6 +4169,7 @@ def get_main_menu_items():
"83",
f"Toggle Pushover Notifications [{'ON' if _notify.get_settings().enabled else 'OFF'}]",
),
("84", "Send Test Pushover Notification"),
("90", "Download rules from Hashmob.net"),
("91", "Analyze Hashcat Rules"),
("92", "Download wordlists from Hashmob.net"),
@@ -4180,6 +4214,7 @@ def get_main_menu_options():
"80": wordlist_tools_submenu,
"81": rule_tools_submenu,
"83": toggle_notifications,
"84": test_pushover_notification,
"90": lambda: download_hashmob_rules(rules_dir=rulesDirectory),
"91": analyze_rules,
"92": download_hashmob_wordlists,
+1
View File
@@ -33,6 +33,7 @@ MENU_OPTION_TEST_CASES = [
("80", CLI_MODULE._attacks, "wordlist_tools_submenu", "wordlist-tools"),
("81", CLI_MODULE._attacks, "rule_tools_submenu", "rule-tools"),
("83", CLI_MODULE, "toggle_notifications", "toggle-notifications"),
("84", CLI_MODULE, "test_pushover_notification", "test-pushover"),
("90", CLI_MODULE, "download_hashmob_rules", "hashmob-rules"),
("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"),
("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"),
+79
View File
@@ -0,0 +1,79 @@
"""Unit tests for main.test_pushover_notification().
These tests patch ``hate_crack.main._notify`` directly rather than
``hate_crack.notify._send_pushover``. The latter is fragile because
``tests/test_random_rules_attack.py`` purges ``hate_crack.*`` from
``sys.modules`` and re-imports, which leaves ``main._notify`` pointing
at a different module object than the one a top-level ``patch`` would
touch. Patching the attribute on ``main`` itself is robust to that.
We use ``patch.object(hc_main, "_notify")`` rather than
``patch("hate_crack.main._notify")`` so the patch target is the exact
module object whose function we invoke. A string target would re-resolve
``hate_crack.main`` through ``sys.modules``, which — again thanks to the
purge in ``test_random_rules_attack.py`` — can be a different object from
the ``hc_main`` reference bound at test-module import time.
"""
from types import SimpleNamespace
from unittest.mock import patch
from hate_crack import main as hc_main
def _settings(
*, enabled: bool = True, token: str = "tok", user: str = "usr"
) -> SimpleNamespace:
"""Minimal stand-in for ``NotifySettings`` — we only read three fields."""
return SimpleNamespace(enabled=enabled, pushover_token=token, pushover_user=user)
class TestTestPushoverNotification:
def test_success_prints_confirmation_and_sends(self, capsys):
with patch.object(hc_main, "_notify") as mock_notify:
mock_notify.get_settings.return_value = _settings(enabled=True)
mock_notify._send_pushover.return_value = True
hc_main.test_pushover_notification()
mock_notify._send_pushover.assert_called_once()
args = mock_notify._send_pushover.call_args.args
assert args[0] == "tok"
assert args[1] == "usr"
assert args[2] == "hate_crack: test notification"
assert "test notification from hate_crack" in args[3]
out = capsys.readouterr().out
assert "[+] Test Pushover notification sent" in out
def test_failure_prints_failure_line(self, capsys):
with patch.object(hc_main, "_notify") as mock_notify:
mock_notify.get_settings.return_value = _settings(enabled=True)
mock_notify._send_pushover.return_value = False
hc_main.test_pushover_notification()
out = capsys.readouterr().out
assert "[!] Test Pushover notification failed" in out
def test_missing_token_skips_send_and_warns(self, capsys):
with patch.object(hc_main, "_notify") as mock_notify:
mock_notify.get_settings.return_value = _settings(enabled=True, token="")
hc_main.test_pushover_notification()
mock_notify._send_pushover.assert_not_called()
out = capsys.readouterr().out
assert "[!] Pushover credentials missing" in out
assert "notify_pushover_token" in out
def test_missing_user_skips_send_and_warns(self, capsys):
with patch.object(hc_main, "_notify") as mock_notify:
mock_notify.get_settings.return_value = _settings(enabled=True, user="")
hc_main.test_pushover_notification()
mock_notify._send_pushover.assert_not_called()
out = capsys.readouterr().out
assert "[!] Pushover credentials missing" in out
def test_globally_off_still_sends_with_note(self, capsys):
with patch.object(hc_main, "_notify") as mock_notify:
mock_notify.get_settings.return_value = _settings(enabled=False)
mock_notify._send_pushover.return_value = True
hc_main.test_pushover_notification()
mock_notify._send_pushover.assert_called_once()
out = capsys.readouterr().out
assert "notifications are globally OFF" in out
assert "[+] Test Pushover notification sent" in out