feat(notify): persist notify_per_crack_enabled atomically

Add save_per_crack_enabled() as a data-layer sibling to save_enabled(),
using the same _atomic_rewrite primitive so mid-write crashes cannot
corrupt config.json.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-04-22 18:48:24 -04:00
co-authored by Claude Sonnet 4.6
parent eae45517b3
commit 8a148d5261
2 changed files with 39 additions and 0 deletions
+9
View File
@@ -147,6 +147,15 @@ def save_enabled(config_path: str, enabled: bool) -> None:
_atomic_rewrite(config_path, _apply)
def save_per_crack_enabled(config_path: str, enabled: bool) -> None:
"""Persist ``notify_per_crack_enabled`` without disturbing other config keys."""
def _apply(data: dict) -> None:
data["notify_per_crack_enabled"] = bool(enabled)
_atomic_rewrite(config_path, _apply)
def add_to_allowlist(config_path: str, attack_name: str) -> None:
"""Append ``attack_name`` to ``notify_attack_allowlist`` if absent.
+30
View File
@@ -7,6 +7,7 @@ from hate_crack.notify.settings import (
add_to_allowlist,
load_settings,
save_enabled,
save_per_crack_enabled,
)
@@ -157,3 +158,32 @@ class TestAddToAllowlist:
add_to_allowlist(str(config_path), "Brute Force")
data = json.loads(config_path.read_text())
assert data["notify_attack_allowlist"] == ["Brute Force"]
class TestSavePerCrackEnabled:
def test_writes_new_config(self, tmp_path: Path) -> None:
config_path = tmp_path / "config.json"
save_per_crack_enabled(str(config_path), True)
data = json.loads(config_path.read_text())
assert data["notify_per_crack_enabled"] is True
def test_preserves_existing_keys(self, tmp_path: Path) -> None:
config_path = tmp_path / "config.json"
initial = {
"hcatBin": "hashcat",
"notify_enabled": True,
"notify_per_crack_enabled": False,
}
config_path.write_text(json.dumps(initial))
save_per_crack_enabled(str(config_path), True)
data = json.loads(config_path.read_text())
assert data["hcatBin"] == "hashcat"
assert data["notify_enabled"] is True
assert data["notify_per_crack_enabled"] is True
def test_toggles_back_and_forth(self, tmp_path: Path) -> None:
config_path = tmp_path / "config.json"
save_per_crack_enabled(str(config_path), True)
save_per_crack_enabled(str(config_path), False)
data = json.loads(config_path.read_text())
assert data["notify_per_crack_enabled"] is False