mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
feat(notify): add toggle_per_crack_enabled runtime toggle
Promotes notify_per_crack_enabled from config-file-only to a runtime toggle with the same style (global-decl, default-init, OSError-via-logger) as the existing toggle_enabled, with full TDD coverage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
8a148d5261
commit
e644e54b4b
@@ -49,6 +49,7 @@ from hate_crack.notify.settings import (
|
||||
add_to_allowlist,
|
||||
load_settings,
|
||||
save_enabled,
|
||||
save_per_crack_enabled,
|
||||
)
|
||||
from hate_crack.notify.tailer import (
|
||||
CrackTailer,
|
||||
@@ -91,6 +92,7 @@ __all__ = [
|
||||
"stop_tailer",
|
||||
"suppressed_notifications",
|
||||
"toggle_enabled",
|
||||
"toggle_per_crack_enabled",
|
||||
"_send_pushover",
|
||||
]
|
||||
|
||||
@@ -145,6 +147,24 @@ def toggle_enabled() -> bool:
|
||||
return _settings.enabled
|
||||
|
||||
|
||||
def toggle_per_crack_enabled() -> bool:
|
||||
"""Flip ``notify_per_crack_enabled``, persist to ``config.json``, return new state.
|
||||
|
||||
If ``init`` was never called we still toggle an in-memory default — the
|
||||
UI update must not crash even if the config file is unreachable.
|
||||
"""
|
||||
global _settings
|
||||
if _settings is None:
|
||||
_settings = NotifySettings()
|
||||
_settings.per_crack_enabled = not _settings.per_crack_enabled
|
||||
if _config_path:
|
||||
try:
|
||||
save_per_crack_enabled(_config_path, _settings.per_crack_enabled)
|
||||
except OSError as exc:
|
||||
logger.warning("Could not persist notify_per_crack_enabled: %s", exc)
|
||||
return _settings.per_crack_enabled
|
||||
|
||||
|
||||
def _in_allowlist(attack_name: str) -> bool:
|
||||
return attack_name in get_settings().attack_allowlist
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Unit tests for the toggle_per_crack_enabled runtime toggle."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from hate_crack import notify as _notify
|
||||
|
||||
|
||||
def _init_with(tmp_path: Path, **overrides) -> Path:
|
||||
"""Seed a config file with defaults + overrides and init the notify module."""
|
||||
config_path = tmp_path / "config.json"
|
||||
cfg = {
|
||||
"notify_enabled": False,
|
||||
"notify_per_crack_enabled": False,
|
||||
"notify_pushover_token": "",
|
||||
"notify_pushover_user": "",
|
||||
}
|
||||
cfg.update(overrides)
|
||||
config_path.write_text(json.dumps(cfg))
|
||||
_notify.init(str(config_path), cfg)
|
||||
return config_path
|
||||
|
||||
|
||||
class TestTogglePerCrackEnabled:
|
||||
def test_off_to_on_flips_and_persists(self, tmp_path: Path) -> None:
|
||||
config_path = _init_with(tmp_path)
|
||||
try:
|
||||
new_state = _notify.toggle_per_crack_enabled()
|
||||
assert new_state is True
|
||||
assert _notify.get_settings().per_crack_enabled is True
|
||||
data = json.loads(config_path.read_text())
|
||||
assert data["notify_per_crack_enabled"] is True
|
||||
finally:
|
||||
_notify.clear_state_for_tests()
|
||||
|
||||
def test_on_to_off_flips_and_persists(self, tmp_path: Path) -> None:
|
||||
config_path = _init_with(tmp_path, notify_per_crack_enabled=True)
|
||||
try:
|
||||
new_state = _notify.toggle_per_crack_enabled()
|
||||
assert new_state is False
|
||||
assert _notify.get_settings().per_crack_enabled is False
|
||||
data = json.loads(config_path.read_text())
|
||||
assert data["notify_per_crack_enabled"] is False
|
||||
finally:
|
||||
_notify.clear_state_for_tests()
|
||||
|
||||
def test_toggle_without_init_uses_defaults(self) -> None:
|
||||
# Mirrors the behavior of toggle_enabled: must not crash when init
|
||||
# was never called. The toggle flips an in-memory default; nothing
|
||||
# is persisted because _config_path is None.
|
||||
try:
|
||||
_notify.clear_state_for_tests()
|
||||
new_state = _notify.toggle_per_crack_enabled()
|
||||
assert new_state is True
|
||||
assert _notify.get_settings().per_crack_enabled is True
|
||||
finally:
|
||||
_notify.clear_state_for_tests()
|
||||
|
||||
def test_does_not_touch_global_enabled(self, tmp_path: Path) -> None:
|
||||
config_path = _init_with(tmp_path, notify_enabled=False)
|
||||
try:
|
||||
_notify.toggle_per_crack_enabled()
|
||||
data = json.loads(config_path.read_text())
|
||||
# notify_enabled stays False; only per-crack flipped.
|
||||
assert data["notify_enabled"] is False
|
||||
assert data["notify_per_crack_enabled"] is True
|
||||
finally:
|
||||
_notify.clear_state_for_tests()
|
||||
Reference in New Issue
Block a user