mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-04-28 03:53:10 -07:00
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:
372
.claude/plans/2026-04-22-test-pushover-menu.md
Normal file
372
.claude/plans/2026-04-22-test-pushover-menu.md
Normal file
@@ -0,0 +1,372 @@
|
||||
# Test Pushover Menu Entry Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add main-menu entry `84. Send Test Pushover Notification` that sends a canned Pushover message to verify the user's credentials and network path.
|
||||
|
||||
**Architecture:** One new function `test_pushover_notification()` in `hate_crack/main.py` that reads settings from the `notify` subsystem, calls the existing `notify._send_pushover(...)` once, and prints a one-line status to stdout. Two menu tables are updated (the main map in `main.py` and the duplicate in `hate_crack.py`). Per the spec, the test ignores the `notify_enabled` global toggle — if the toggle is OFF, we still send, but prepend an informational line so the user is not confused later.
|
||||
|
||||
**Tech Stack:** Python 3, pytest, `unittest.mock.patch`, existing `hate_crack.notify` package.
|
||||
|
||||
**Spec:** `.claude/specs/2026-04-22-test-pushover-menu-design.md`
|
||||
|
||||
**Worktree:** `/tmp/hate_crack-test-pushover` on branch `feat/test-pushover-menu`.
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
**New files:**
|
||||
- `tests/test_ui_test_pushover.py` — unit tests for `test_pushover_notification()`
|
||||
|
||||
**Modified files:**
|
||||
- `hate_crack/main.py`
|
||||
- Add function `test_pushover_notification()` near the existing `toggle_notifications()` (around line 4091)
|
||||
- Add `("84", "Send Test Pushover Notification")` row to `get_main_menu_items()`
|
||||
- Add `"84": test_pushover_notification` entry to `get_main_menu_options()`
|
||||
- `hate_crack.py`
|
||||
- Add `"84": test_pushover_notification` to the duplicate `get_main_menu_options()` (resolved via `__getattr__` proxy)
|
||||
- `tests/test_ui_menu_options.py`
|
||||
- Add `("84", CLI_MODULE, "test_pushover_notification", "test-pushover")` to `MENU_OPTION_TEST_CASES`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add `test_pushover_notification()` function (TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_ui_test_pushover.py`
|
||||
- Modify: `hate_crack/main.py` (new function near line 4091, after `toggle_notifications`)
|
||||
|
||||
### Step 1: Write the failing test file
|
||||
|
||||
- [ ] Create `tests/test_ui_test_pushover.py` with the full test module below.
|
||||
|
||||
```python
|
||||
"""Unit tests for main.test_pushover_notification()."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from hate_crack import main as hc_main
|
||||
from hate_crack import notify
|
||||
from hate_crack.notify.settings import NotifySettings
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_notify_state():
|
||||
notify.clear_state_for_tests()
|
||||
yield
|
||||
notify.clear_state_for_tests()
|
||||
|
||||
|
||||
def _install_settings(
|
||||
*,
|
||||
enabled: bool = True,
|
||||
token: str = "tok",
|
||||
user: str = "usr",
|
||||
) -> None:
|
||||
"""Swap fresh settings into the notify module for this test."""
|
||||
settings = NotifySettings()
|
||||
settings.enabled = enabled
|
||||
settings.pushover_token = token
|
||||
settings.pushover_user = user
|
||||
notify._settings = settings
|
||||
|
||||
|
||||
class TestTestPushoverNotification:
|
||||
def test_success_prints_confirmation_and_sends(self, capsys):
|
||||
_install_settings(enabled=True, token="tok", user="usr")
|
||||
with patch("hate_crack.notify._send_pushover", return_value=True) as send:
|
||||
hc_main.test_pushover_notification()
|
||||
assert send.called
|
||||
args = send.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):
|
||||
_install_settings(enabled=True, token="tok", user="usr")
|
||||
with patch("hate_crack.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):
|
||||
_install_settings(enabled=True, token="", user="usr")
|
||||
with patch("hate_crack.notify._send_pushover") as send:
|
||||
hc_main.test_pushover_notification()
|
||||
send.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):
|
||||
_install_settings(enabled=True, token="tok", user="")
|
||||
with patch("hate_crack.notify._send_pushover") as send:
|
||||
hc_main.test_pushover_notification()
|
||||
send.assert_not_called()
|
||||
out = capsys.readouterr().out
|
||||
assert "[!] Pushover credentials missing" in out
|
||||
|
||||
def test_globally_off_still_sends_with_note(self, capsys):
|
||||
_install_settings(enabled=False, token="tok", user="usr")
|
||||
with patch("hate_crack.notify._send_pushover", return_value=True) as send:
|
||||
hc_main.test_pushover_notification()
|
||||
send.assert_called_once()
|
||||
out = capsys.readouterr().out
|
||||
assert "notifications are globally OFF" in out
|
||||
assert "[+] Test Pushover notification sent" in out
|
||||
```
|
||||
|
||||
### Step 2: Run tests to confirm they fail
|
||||
|
||||
- [ ] Run: `HATE_CRACK_SKIP_INIT=1 uv run pytest tests/test_ui_test_pushover.py -v`
|
||||
- [ ] Expected: all five tests fail with `AttributeError: module 'hate_crack.main' has no attribute 'test_pushover_notification'`.
|
||||
|
||||
### Step 3: Implement `test_pushover_notification()` in `main.py`
|
||||
|
||||
- [ ] Open `hate_crack/main.py` and locate the `toggle_notifications()` function (around line 4091).
|
||||
- [ ] Insert the following function directly below `toggle_notifications()` (i.e., between `toggle_notifications` and `get_main_menu_items`):
|
||||
|
||||
```python
|
||||
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.")
|
||||
```
|
||||
|
||||
### Step 4: Run the new tests to confirm they pass
|
||||
|
||||
- [ ] Run: `HATE_CRACK_SKIP_INIT=1 uv run pytest tests/test_ui_test_pushover.py -v`
|
||||
- [ ] Expected: all five tests pass.
|
||||
|
||||
### Step 5: Commit
|
||||
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
git add tests/test_ui_test_pushover.py hate_crack/main.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(notify): add test_pushover_notification helper
|
||||
|
||||
Canned send path so a user can verify Pushover credentials without
|
||||
running an attack. Ignores the global notify_enabled toggle — the test's
|
||||
purpose is to confirm the pipe is live, not that attack notifications
|
||||
are enabled. Prints a note when the global toggle is OFF so the user is
|
||||
not confused later.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Wire the entry into `main.py`'s menu tables
|
||||
|
||||
**Files:**
|
||||
- Modify: `hate_crack/main.py` — two edits inside `get_main_menu_items()` and `get_main_menu_options()`
|
||||
|
||||
### Step 1: Add the menu item row
|
||||
|
||||
- [ ] Open `hate_crack/main.py` and find the `("83", f"Toggle Pushover Notifications [...]")` entry inside `get_main_menu_items()` (around line 4135).
|
||||
- [ ] Directly below the closing `)` of that 83 tuple, insert:
|
||||
|
||||
```python
|
||||
("84", "Send Test Pushover Notification"),
|
||||
```
|
||||
|
||||
Context (before):
|
||||
|
||||
```python
|
||||
(
|
||||
"83",
|
||||
f"Toggle Pushover Notifications [{'ON' if _notify.get_settings().enabled else 'OFF'}]",
|
||||
),
|
||||
("90", "Download rules from Hashmob.net"),
|
||||
```
|
||||
|
||||
Context (after):
|
||||
|
||||
```python
|
||||
(
|
||||
"83",
|
||||
f"Toggle Pushover Notifications [{'ON' if _notify.get_settings().enabled else 'OFF'}]",
|
||||
),
|
||||
("84", "Send Test Pushover Notification"),
|
||||
("90", "Download rules from Hashmob.net"),
|
||||
```
|
||||
|
||||
### Step 2: Add the options map entry
|
||||
|
||||
- [ ] In the same file find `"83": toggle_notifications,` inside `get_main_menu_options()` (around line 4182).
|
||||
- [ ] Insert on the next line:
|
||||
|
||||
```python
|
||||
"84": test_pushover_notification,
|
||||
```
|
||||
|
||||
Context (after):
|
||||
|
||||
```python
|
||||
"83": toggle_notifications,
|
||||
"84": test_pushover_notification,
|
||||
"90": lambda: download_hashmob_rules(rules_dir=rulesDirectory),
|
||||
```
|
||||
|
||||
### Step 3: Sanity-check main.py loads
|
||||
|
||||
- [ ] Run: `HATE_CRACK_SKIP_INIT=1 uv run python -c "from hate_crack import main; print(main.get_main_menu_options()['84'].__name__)"`
|
||||
- [ ] Expected output ends with: `test_pushover_notification`
|
||||
|
||||
### Step 4: Commit
|
||||
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
git add hate_crack/main.py
|
||||
git commit -m "feat(notify): wire option 84 into main.py menu"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Wire the entry into `hate_crack.py`'s duplicate menu
|
||||
|
||||
**Files:**
|
||||
- Modify: `hate_crack.py` — one edit inside `get_main_menu_options()`
|
||||
|
||||
### Step 1: Add the options map entry
|
||||
|
||||
- [ ] Open `hate_crack.py` (repo root) and find `"83": toggle_notifications,` inside `get_main_menu_options()` (line 97).
|
||||
- [ ] Insert on the next line:
|
||||
|
||||
```python
|
||||
"84": test_pushover_notification,
|
||||
```
|
||||
|
||||
Note: `test_pushover_notification` is not explicitly imported in `hate_crack.py`; it resolves via the module's `__getattr__` proxy to `_main`. No import changes are needed.
|
||||
|
||||
Context (after):
|
||||
|
||||
```python
|
||||
"81": _attacks.rule_tools_submenu,
|
||||
"83": toggle_notifications,
|
||||
"84": test_pushover_notification,
|
||||
"90": download_hashmob_rules,
|
||||
```
|
||||
|
||||
### Step 2: Sanity-check both menus agree on option 84
|
||||
|
||||
- [ ] Run: `HATE_CRACK_SKIP_INIT=1 uv run python -c "import importlib.util, pathlib; spec = importlib.util.spec_from_file_location('cli', pathlib.Path('hate_crack.py')); m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); print(m.get_main_menu_options()['84'].__name__)"`
|
||||
- [ ] Expected output ends with: `test_pushover_notification`
|
||||
|
||||
### Step 3: Commit
|
||||
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
git add hate_crack.py
|
||||
git commit -m "feat(notify): wire option 84 into hate_crack.py proxy menu"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Add parametrized menu-wiring test
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test_ui_menu_options.py` — one row added to `MENU_OPTION_TEST_CASES`
|
||||
|
||||
### Step 1: Add the test case row
|
||||
|
||||
- [ ] Open `tests/test_ui_menu_options.py` and find the line:
|
||||
|
||||
```python
|
||||
("83", CLI_MODULE, "toggle_notifications", "toggle-notifications"),
|
||||
```
|
||||
|
||||
- [ ] Insert directly below it:
|
||||
|
||||
```python
|
||||
("84", CLI_MODULE, "test_pushover_notification", "test-pushover"),
|
||||
```
|
||||
|
||||
### Step 2: Run the menu test suite
|
||||
|
||||
- [ ] Run: `HATE_CRACK_SKIP_INIT=1 uv run pytest tests/test_ui_menu_options.py -v`
|
||||
- [ ] Expected: the new parametrized case `test_main_menu_option_returns_expected[84-...-test-pushover]` passes, and no existing cases regress.
|
||||
|
||||
### Step 3: Commit
|
||||
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
git add tests/test_ui_menu_options.py
|
||||
git commit -m "test(notify): cover option 84 in menu wiring parametrize"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Full verification
|
||||
|
||||
**Files:** None. This task verifies the whole feature.
|
||||
|
||||
### Step 1: Run the full test suite
|
||||
|
||||
- [ ] Run: `HATE_CRACK_SKIP_INIT=1 uv run pytest -v`
|
||||
- [ ] Expected: all tests pass. No new failures compared to the baseline on `feat/pushover-notifications`.
|
||||
|
||||
### Step 2: Run lint
|
||||
|
||||
- [ ] Run: `uv run ruff check hate_crack`
|
||||
- [ ] Expected: no errors. If `ty` is available, also run `uv run ty check hate_crack`.
|
||||
|
||||
### Step 3: Manual menu display spot-check (optional)
|
||||
|
||||
- [ ] Run: `HATE_CRACK_SKIP_INIT=1 uv run python -c "from hate_crack import main; [print(f'{k}: {v}') for k, v in main.get_main_menu_items()]" | grep -E "^(83|84)"`
|
||||
- [ ] Expected output (token/user state may flip ON/OFF):
|
||||
|
||||
```
|
||||
83: Toggle Pushover Notifications [OFF]
|
||||
84: Send Test Pushover Notification
|
||||
```
|
||||
|
||||
### Step 4: No-op if prior tasks already committed cleanly
|
||||
|
||||
- [ ] Run: `git status`
|
||||
- [ ] Expected: `nothing to commit, working tree clean`. Branch has four commits ahead of `feat/pushover-notifications` (the four task commits).
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Results
|
||||
|
||||
- **Spec coverage:** all four spec sections are covered — key `84` and label (Task 2+3), missing-creds/success/failure branches (Task 1), globally-OFF behavior (Task 1), menu wiring tests (Task 4). The unit test for the function is in a dedicated new file rather than extended into `test_notify.py` (the spec allowed either).
|
||||
- **Placeholder scan:** no TBD/TODO tokens; every code step has literal code; every run step has a literal command and expected output.
|
||||
- **Type consistency:** the function name `test_pushover_notification` is used identically across Tasks 1, 2, 3, and 4. The constants `"hate_crack: test notification"` and `"test notification from hate_crack"` appear verbatim in both the test assertions (Task 1 Step 1) and the implementation (Task 1 Step 3).
|
||||
96
.claude/specs/2026-04-22-test-pushover-menu-design.md
Normal file
96
.claude/specs/2026-04-22-test-pushover-menu-design.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Test Pushover Notification — Menu Entry
|
||||
|
||||
**Date:** 2026-04-22
|
||||
**Branch:** `feat/test-pushover-menu`
|
||||
|
||||
## Goal
|
||||
|
||||
Give the user a one-click way to verify that their Pushover credentials and
|
||||
network path actually work, independent of running an attack.
|
||||
|
||||
## User-facing behavior
|
||||
|
||||
Main menu gains one new entry:
|
||||
|
||||
```
|
||||
84. Send Test Pushover Notification
|
||||
```
|
||||
|
||||
Placed directly below the existing `83. Toggle Pushover Notifications`
|
||||
entry so related controls sit together.
|
||||
|
||||
Selecting it calls `_send_pushover(...)` once with a canned title/message:
|
||||
|
||||
- **Title:** `hate_crack: test notification`
|
||||
- **Message:** `This is a test notification from hate_crack. If you see this, Pushover is wired up correctly.`
|
||||
|
||||
It then prints one of three lines to the terminal:
|
||||
|
||||
1. **Credentials missing** — `notify_pushover_token` or `notify_pushover_user`
|
||||
is empty in `config.json`:
|
||||
> `[!] Pushover credentials missing. Set notify_pushover_token and notify_pushover_user in config.json.`
|
||||
2. **Send succeeded** — `_send_pushover` returned `True`:
|
||||
> `[+] Test Pushover notification sent. Check your device.`
|
||||
3. **Send failed** — `_send_pushover` returned `False` (HTTP non-200, network
|
||||
error, missing `requests`, etc.):
|
||||
> `[!] Test Pushover notification failed. See log output for details.`
|
||||
|
||||
### Global toggle behavior (option A)
|
||||
|
||||
The test **ignores** the `notify_enabled` global toggle. The entire purpose
|
||||
of the test is to verify the pipe works, and forcing the user to flip the
|
||||
toggle first would be friction for no benefit.
|
||||
|
||||
When the global toggle is OFF, we still send, but prepend an informational
|
||||
line so the user is not confused later when no attack notifications arrive:
|
||||
|
||||
> `(notifications are globally OFF, but sending test anyway)`
|
||||
|
||||
## Implementation outline
|
||||
|
||||
Follows the existing three-layer pattern for non-attack menu items (same
|
||||
shape as `toggle_notifications`):
|
||||
|
||||
1. **`hate_crack/main.py`** — new function `test_pushover_notification()`:
|
||||
- Read `_notify.get_settings()` for token/user.
|
||||
- Empty-creds branch -> print the missing-creds warning, return.
|
||||
- If `settings.enabled is False`, print the "globally OFF" note.
|
||||
- Call `_notify._send_pushover(token, user, title, message)`.
|
||||
- Print success or failure line based on the returned bool.
|
||||
2. **`hate_crack/main.py`** — menu wiring in `get_main_menu_items()` and
|
||||
`get_main_menu_options()`:
|
||||
- Add `("84", "Send Test Pushover Notification")` right after the `83`
|
||||
entry.
|
||||
- Add `"84": test_pushover_notification` to the options map.
|
||||
3. **`hate_crack.py`** — menu wiring in its duplicate
|
||||
`get_main_menu_options()`:
|
||||
- Add `"84": test_pushover_notification`. Per the proxy pattern,
|
||||
`test_pushover_notification` resolves through `__getattr__`.
|
||||
|
||||
No new module, no new dependency, no new config key.
|
||||
|
||||
## Testing
|
||||
|
||||
Add one test in `tests/test_notify.py` (or a new `tests/test_menu_test_pushover.py`
|
||||
if the existing file is already dense):
|
||||
|
||||
- Monkeypatch `hate_crack.notify._send_pushover` to a stub returning `True`,
|
||||
call `main.test_pushover_notification()`, assert stdout contains the
|
||||
success line and the stub saw the canned title/message.
|
||||
- Stub returning `False` -> assert failure line.
|
||||
- Empty creds (`NotifySettings()` with blank token/user) -> assert
|
||||
missing-creds line and stub **not called**.
|
||||
- `enabled = False` with good creds -> assert the "globally OFF" note
|
||||
appears and the stub **is** called.
|
||||
|
||||
Menu-wiring test: extend `test_ui_menu_options.py` to assert `"84"` is in
|
||||
both the `main.get_main_menu_options()` dict and the `hate_crack.py`
|
||||
duplicate, and that both resolve to a callable.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No new backend (Slack, webhooks) — a sibling test for those can be added
|
||||
when those backends exist.
|
||||
- No interactive "enter token" prompt; the test assumes `config.json` is
|
||||
already populated, matching how `toggle_notifications` behaves.
|
||||
- No persistent state changes from running the test.
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
tests/test_ui_test_pushover.py
Normal file
79
tests/test_ui_test_pushover.py
Normal 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
|
||||
Reference in New Issue
Block a user