/ tests / hermes_cli / test_webhook_cli.py
test_webhook_cli.py
  1  """Tests for hermes_cli/webhook.py — webhook subscription CLI."""
  2  
  3  import json
  4  import os
  5  import pytest
  6  from argparse import Namespace
  7  from pathlib import Path
  8  
  9  from hermes_cli.webhook import (
 10      webhook_command,
 11      _load_subscriptions,
 12      _save_subscriptions,
 13      _subscriptions_path,
 14      _is_webhook_enabled,
 15  )
 16  
 17  
 18  @pytest.fixture(autouse=True)
 19  def _isolate(tmp_path, monkeypatch):
 20      monkeypatch.setenv("HERMES_HOME", str(tmp_path))
 21      # Default: webhooks enabled (most tests need this)
 22      monkeypatch.setattr(
 23          "hermes_cli.webhook._is_webhook_enabled", lambda: True
 24      )
 25  
 26  
 27  def _make_args(**kwargs):
 28      defaults = {
 29          "webhook_action": None,
 30          "name": "",
 31          "prompt": "",
 32          "events": "",
 33          "description": "",
 34          "skills": "",
 35          "deliver": "log",
 36          "deliver_chat_id": "",
 37          "secret": "",
 38          "payload": "",
 39      }
 40      defaults.update(kwargs)
 41      return Namespace(**defaults)
 42  
 43  
 44  class TestSubscribe:
 45      def test_basic_create(self, capsys):
 46          webhook_command(_make_args(webhook_action="subscribe", name="test-hook"))
 47          out = capsys.readouterr().out
 48          assert "Created" in out
 49          assert "/webhooks/test-hook" in out
 50          subs = _load_subscriptions()
 51          assert "test-hook" in subs
 52  
 53      def test_with_options(self, capsys):
 54          webhook_command(_make_args(
 55              webhook_action="subscribe",
 56              name="gh-issues",
 57              events="issues,pull_request",
 58              prompt="Issue: {issue.title}",
 59              deliver="telegram",
 60              deliver_chat_id="12345",
 61              description="Watch GitHub",
 62          ))
 63          subs = _load_subscriptions()
 64          route = subs["gh-issues"]
 65          assert route["events"] == ["issues", "pull_request"]
 66          assert route["prompt"] == "Issue: {issue.title}"
 67          assert route["deliver"] == "telegram"
 68          assert route["deliver_extra"] == {"chat_id": "12345"}
 69  
 70      def test_custom_secret(self):
 71          webhook_command(_make_args(
 72              webhook_action="subscribe", name="s", secret="my-secret"
 73          ))
 74          assert _load_subscriptions()["s"]["secret"] == "my-secret"
 75  
 76      def test_auto_secret(self):
 77          webhook_command(_make_args(webhook_action="subscribe", name="s"))
 78          secret = _load_subscriptions()["s"]["secret"]
 79          assert len(secret) > 20
 80  
 81      def test_update(self, capsys):
 82          webhook_command(_make_args(webhook_action="subscribe", name="x", prompt="v1"))
 83          webhook_command(_make_args(webhook_action="subscribe", name="x", prompt="v2"))
 84          out = capsys.readouterr().out
 85          assert "Updated" in out
 86          assert _load_subscriptions()["x"]["prompt"] == "v2"
 87  
 88      def test_invalid_name(self, capsys):
 89          webhook_command(_make_args(webhook_action="subscribe", name="bad name!"))
 90          out = capsys.readouterr().out
 91          assert "Error" in out or "Invalid" in out
 92          assert _load_subscriptions() == {}
 93  
 94  
 95  class TestList:
 96      def test_empty(self, capsys):
 97          webhook_command(_make_args(webhook_action="list"))
 98          out = capsys.readouterr().out
 99          assert "No dynamic" in out
100  
101      def test_with_entries(self, capsys):
102          webhook_command(_make_args(webhook_action="subscribe", name="a"))
103          webhook_command(_make_args(webhook_action="subscribe", name="b"))
104          capsys.readouterr()  # clear
105          webhook_command(_make_args(webhook_action="list"))
106          out = capsys.readouterr().out
107          assert "2 webhook" in out
108          assert "a" in out
109          assert "b" in out
110  
111  
112  class TestRemove:
113      def test_remove_existing(self, capsys):
114          webhook_command(_make_args(webhook_action="subscribe", name="temp"))
115          webhook_command(_make_args(webhook_action="remove", name="temp"))
116          out = capsys.readouterr().out
117          assert "Removed" in out
118          assert _load_subscriptions() == {}
119  
120      def test_remove_nonexistent(self, capsys):
121          webhook_command(_make_args(webhook_action="remove", name="nope"))
122          out = capsys.readouterr().out
123          assert "No subscription" in out
124  
125      def test_selective_remove(self):
126          webhook_command(_make_args(webhook_action="subscribe", name="keep"))
127          webhook_command(_make_args(webhook_action="subscribe", name="drop"))
128          webhook_command(_make_args(webhook_action="remove", name="drop"))
129          subs = _load_subscriptions()
130          assert "keep" in subs
131          assert "drop" not in subs
132  
133  
134  class TestPersistence:
135      def test_file_written(self):
136          webhook_command(_make_args(webhook_action="subscribe", name="persist"))
137          path = _subscriptions_path()
138          assert path.exists()
139          data = json.loads(path.read_text())
140          assert "persist" in data
141  
142      def test_corrupted_file(self):
143          path = _subscriptions_path()
144          path.parent.mkdir(parents=True, exist_ok=True)
145          path.write_text("broken{{{")
146          assert _load_subscriptions() == {}
147  
148  
149  class TestWebhookEnabledGate:
150      def test_blocks_when_disabled(self, capsys, monkeypatch):
151          monkeypatch.setattr("hermes_cli.webhook._is_webhook_enabled", lambda: False)
152          webhook_command(_make_args(webhook_action="subscribe", name="blocked"))
153          out = capsys.readouterr().out
154          assert "not enabled" in out.lower()
155          assert "hermes gateway setup" in out
156          assert _load_subscriptions() == {}
157  
158      def test_blocks_list_when_disabled(self, capsys, monkeypatch):
159          monkeypatch.setattr("hermes_cli.webhook._is_webhook_enabled", lambda: False)
160          webhook_command(_make_args(webhook_action="list"))
161          out = capsys.readouterr().out
162          assert "not enabled" in out.lower()
163  
164      def test_allows_when_enabled(self, capsys):
165          # _is_webhook_enabled already patched to True by autouse fixture
166          webhook_command(_make_args(webhook_action="subscribe", name="allowed"))
167          out = capsys.readouterr().out
168          assert "Created" in out
169          assert "allowed" in _load_subscriptions()
170  
171      def test_real_check_disabled(self, monkeypatch):
172          monkeypatch.setattr(
173              "hermes_cli.webhook._get_webhook_config",
174              lambda: {},
175          )
176          monkeypatch.setattr(
177              "hermes_cli.webhook._is_webhook_enabled",
178              lambda: bool({}.get("enabled")),
179          )
180          import hermes_cli.webhook as wh_mod
181          assert wh_mod._is_webhook_enabled() is False
182  
183      def test_real_check_enabled(self, monkeypatch):
184          monkeypatch.setattr(
185              "hermes_cli.webhook._is_webhook_enabled",
186              lambda: True,
187          )
188          import hermes_cli.webhook as wh_mod
189          assert wh_mod._is_webhook_enabled() is True