"""Tests for hermes_cli/webhook.py — webhook subscription CLI.""" import json import os import pytest import stat from argparse import Namespace from hermes_cli.webhook import ( webhook_command, _get_webhook_base_url, _load_subscriptions, _save_subscriptions, _subscriptions_path, ) @pytest.fixture(autouse=True) def _isolate(tmp_path, monkeypatch): monkeypatch.setenv("HERMES_HOME", str(tmp_path)) # Default: webhooks enabled (most tests need this) monkeypatch.setattr( "hermes_cli.webhook._is_webhook_enabled", lambda: True ) def _make_args(**kwargs): defaults = { "webhook_action": None, "name": "", "prompt": "", "events": "", "description": "", "skills": "", "deliver": "log", "deliver_chat_id": "", "secret": "", "payload": "", "script": "", } defaults.update(kwargs) return Namespace(**defaults) @pytest.mark.parametrize("host", [None, "", "0.0.0.0", "::"]) def test_webhook_base_url_maps_wildcard_hosts_to_localhost(monkeypatch, host): monkeypatch.setattr( "hermes_cli.webhook._get_webhook_config", lambda: {"extra": {"host": host, "port": 9123}}, ) assert _get_webhook_base_url() == "http://localhost:9123" class TestSubscribe: def test_custom_secret(self): webhook_command(_make_args( webhook_action="subscribe", name="s", secret="my-secret" )) assert _load_subscriptions()["s"]["secret"] == "my-secret" def test_auto_secret(self): webhook_command(_make_args(webhook_action="subscribe", name="s")) secret = _load_subscriptions()["s"]["secret"] assert len(secret) > 20 class TestList: def test_with_entries(self, capsys): webhook_command(_make_args(webhook_action="subscribe", name="a")) webhook_command(_make_args(webhook_action="subscribe", name="b")) capsys.readouterr() # clear webhook_command(_make_args(webhook_action="list")) out = capsys.readouterr().out assert "2 webhook" in out assert "a" in out assert "b" in out class TestRemove: def test_selective_remove(self): webhook_command(_make_args(webhook_action="subscribe", name="keep")) webhook_command(_make_args(webhook_action="subscribe", name="drop")) webhook_command(_make_args(webhook_action="remove", name="drop")) subs = _load_subscriptions() assert "keep" in subs assert "drop" not in subs class TestPersistence: def test_corrupted_file(self): path = _subscriptions_path() path.parent.mkdir(parents=True, exist_ok=True) path.write_text("broken{{{") assert _load_subscriptions() == {} @pytest.mark.skipif(os.name == "nt", reason="POSIX mode bits are platform-specific") def test_save_creates_secret_file_owner_only_under_permissive_umask(self): old_umask = os.umask(0o022) try: _save_subscriptions({"demo": {"secret": "TOPSECRET", "prompt": "x"}}) finally: os.umask(old_umask) path = _subscriptions_path() assert stat.S_IMODE(path.stat().st_mode) == 0o600 assert "TOPSECRET" in path.read_text(encoding="utf-8") @pytest.mark.skipif(os.name == "nt", reason="POSIX mode bits are platform-specific") def test_save_narrows_existing_broad_secret_file_mode(self): # Simulate a pre-existing 0o644 file from before this hardening landed. path = _subscriptions_path() path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps({"old": {"secret": "stale", "prompt": "x"}})) path.chmod(0o644) _save_subscriptions({"demo": {"secret": "FRESH", "prompt": "x"}}) assert stat.S_IMODE(path.stat().st_mode) == 0o600 assert "FRESH" in path.read_text(encoding="utf-8") class TestWebhookEnabledGate: def test_blocks_list_when_disabled(self, capsys, monkeypatch): monkeypatch.setattr("hermes_cli.webhook._is_webhook_enabled", lambda: False) webhook_command(_make_args(webhook_action="list")) out = capsys.readouterr().out assert "not enabled" in out.lower() def test_allows_when_enabled(self, capsys): # _is_webhook_enabled already patched to True by autouse fixture webhook_command(_make_args(webhook_action="subscribe", name="allowed")) out = capsys.readouterr().out assert "Created" in out assert "allowed" in _load_subscriptions() def test_real_check_disabled(self, monkeypatch): monkeypatch.setattr( "hermes_cli.webhook._get_webhook_config", lambda: {}, ) monkeypatch.setattr( "hermes_cli.webhook._is_webhook_enabled", lambda: bool({}.get("enabled")), ) import hermes_cli.webhook as wh_mod assert wh_mod._is_webhook_enabled() is False