190 lines
6.5 KiB
Python
190 lines
6.5 KiB
Python
|
|
"""Tests for hermes_cli/webhook.py — webhook subscription CLI."""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import pytest
|
||
|
|
from argparse import Namespace
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from hermes_cli.webhook import (
|
||
|
|
webhook_command,
|
||
|
|
_load_subscriptions,
|
||
|
|
_save_subscriptions,
|
||
|
|
_subscriptions_path,
|
||
|
|
_is_webhook_enabled,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@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": "",
|
||
|
|
}
|
||
|
|
defaults.update(kwargs)
|
||
|
|
return Namespace(**defaults)
|
||
|
|
|
||
|
|
|
||
|
|
class TestSubscribe:
|
||
|
|
def test_basic_create(self, capsys):
|
||
|
|
webhook_command(_make_args(webhook_action="subscribe", name="test-hook"))
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "Created" in out
|
||
|
|
assert "/webhooks/test-hook" in out
|
||
|
|
subs = _load_subscriptions()
|
||
|
|
assert "test-hook" in subs
|
||
|
|
|
||
|
|
def test_with_options(self, capsys):
|
||
|
|
webhook_command(_make_args(
|
||
|
|
webhook_action="subscribe",
|
||
|
|
name="gh-issues",
|
||
|
|
events="issues,pull_request",
|
||
|
|
prompt="Issue: {issue.title}",
|
||
|
|
deliver="telegram",
|
||
|
|
deliver_chat_id="12345",
|
||
|
|
description="Watch GitHub",
|
||
|
|
))
|
||
|
|
subs = _load_subscriptions()
|
||
|
|
route = subs["gh-issues"]
|
||
|
|
assert route["events"] == ["issues", "pull_request"]
|
||
|
|
assert route["prompt"] == "Issue: {issue.title}"
|
||
|
|
assert route["deliver"] == "telegram"
|
||
|
|
assert route["deliver_extra"] == {"chat_id": "12345"}
|
||
|
|
|
||
|
|
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
|
||
|
|
|
||
|
|
def test_update(self, capsys):
|
||
|
|
webhook_command(_make_args(webhook_action="subscribe", name="x", prompt="v1"))
|
||
|
|
webhook_command(_make_args(webhook_action="subscribe", name="x", prompt="v2"))
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "Updated" in out
|
||
|
|
assert _load_subscriptions()["x"]["prompt"] == "v2"
|
||
|
|
|
||
|
|
def test_invalid_name(self, capsys):
|
||
|
|
webhook_command(_make_args(webhook_action="subscribe", name="bad name!"))
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "Error" in out or "Invalid" in out
|
||
|
|
assert _load_subscriptions() == {}
|
||
|
|
|
||
|
|
|
||
|
|
class TestList:
|
||
|
|
def test_empty(self, capsys):
|
||
|
|
webhook_command(_make_args(webhook_action="list"))
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "No dynamic" in out
|
||
|
|
|
||
|
|
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_remove_existing(self, capsys):
|
||
|
|
webhook_command(_make_args(webhook_action="subscribe", name="temp"))
|
||
|
|
webhook_command(_make_args(webhook_action="remove", name="temp"))
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "Removed" in out
|
||
|
|
assert _load_subscriptions() == {}
|
||
|
|
|
||
|
|
def test_remove_nonexistent(self, capsys):
|
||
|
|
webhook_command(_make_args(webhook_action="remove", name="nope"))
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "No subscription" in out
|
||
|
|
|
||
|
|
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_file_written(self):
|
||
|
|
webhook_command(_make_args(webhook_action="subscribe", name="persist"))
|
||
|
|
path = _subscriptions_path()
|
||
|
|
assert path.exists()
|
||
|
|
data = json.loads(path.read_text())
|
||
|
|
assert "persist" in data
|
||
|
|
|
||
|
|
def test_corrupted_file(self):
|
||
|
|
path = _subscriptions_path()
|
||
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
path.write_text("broken{{{")
|
||
|
|
assert _load_subscriptions() == {}
|
||
|
|
|
||
|
|
|
||
|
|
class TestWebhookEnabledGate:
|
||
|
|
def test_blocks_when_disabled(self, capsys, monkeypatch):
|
||
|
|
monkeypatch.setattr("hermes_cli.webhook._is_webhook_enabled", lambda: False)
|
||
|
|
webhook_command(_make_args(webhook_action="subscribe", name="blocked"))
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "not enabled" in out.lower()
|
||
|
|
assert "hermes gateway setup" in out
|
||
|
|
assert _load_subscriptions() == {}
|
||
|
|
|
||
|
|
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
|
||
|
|
|
||
|
|
def test_real_check_enabled(self, monkeypatch):
|
||
|
|
monkeypatch.setattr(
|
||
|
|
"hermes_cli.webhook._is_webhook_enabled",
|
||
|
|
lambda: True,
|
||
|
|
)
|
||
|
|
import hermes_cli.webhook as wh_mod
|
||
|
|
assert wh_mod._is_webhook_enabled() is True
|