mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Fix variable name breakage (run_agent, hermes_constants, etc.) where import rewriter changed 'import X' to 'import hermes_agent.Y' but test code still referenced 'X' as a variable name. Fix package-vs-module confusion (cli.auth, cli.models, cli.ui) where single files became directories. Fix hardcoded file paths in tests pointing to old locations. Fix tool registry to discover tools in subpackage directories. Fix stale import in hermes_agent/tools/__init__.py. Part of #14182, #14183
217 lines
8.7 KiB
Python
217 lines
8.7 KiB
Python
"""Unit tests for hermes_cli/dingtalk_auth.py (QR device-flow registration)."""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API layer — _api_post + error mapping
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestApiPost:
|
|
|
|
def test_raises_on_network_error(self):
|
|
import requests
|
|
from hermes_agent.cli.auth.dingtalk import _api_post, RegistrationError
|
|
|
|
with patch("hermes_agent.cli.auth.dingtalk.requests.post",
|
|
side_effect=requests.ConnectionError("nope")):
|
|
with pytest.raises(RegistrationError, match="Network error"):
|
|
_api_post("/app/registration/init", {"source": "hermes"})
|
|
|
|
def test_raises_on_nonzero_errcode(self):
|
|
from hermes_agent.cli.auth.dingtalk import _api_post, RegistrationError
|
|
|
|
mock_resp = MagicMock()
|
|
mock_resp.raise_for_status = MagicMock()
|
|
mock_resp.json.return_value = {"errcode": 42, "errmsg": "boom"}
|
|
|
|
with patch("hermes_agent.cli.auth.dingtalk.requests.post", return_value=mock_resp):
|
|
with pytest.raises(RegistrationError, match=r"boom \(errcode=42\)"):
|
|
_api_post("/app/registration/init", {"source": "hermes"})
|
|
|
|
def test_returns_data_on_success(self):
|
|
from hermes_agent.cli.auth.dingtalk import _api_post
|
|
|
|
mock_resp = MagicMock()
|
|
mock_resp.raise_for_status = MagicMock()
|
|
mock_resp.json.return_value = {"errcode": 0, "nonce": "abc"}
|
|
|
|
with patch("hermes_agent.cli.auth.dingtalk.requests.post", return_value=mock_resp):
|
|
result = _api_post("/app/registration/init", {"source": "hermes"})
|
|
assert result["nonce"] == "abc"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# begin_registration — 2-step nonce → device_code chain
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBeginRegistration:
|
|
|
|
def test_chains_init_then_begin(self):
|
|
from hermes_agent.cli.auth.dingtalk import begin_registration
|
|
|
|
responses = [
|
|
{"errcode": 0, "nonce": "nonce123"},
|
|
{
|
|
"errcode": 0,
|
|
"device_code": "dev-xyz",
|
|
"verification_uri_complete": "https://open-dev.dingtalk.com/openapp/registration/openClaw?user_code=ABCD",
|
|
"expires_in": 7200,
|
|
"interval": 2,
|
|
},
|
|
]
|
|
with patch("hermes_agent.cli.auth.dingtalk._api_post", side_effect=responses):
|
|
result = begin_registration()
|
|
|
|
assert result["device_code"] == "dev-xyz"
|
|
assert "verification_uri_complete" in result
|
|
assert result["interval"] == 2
|
|
assert result["expires_in"] == 7200
|
|
|
|
def test_missing_nonce_raises(self):
|
|
from hermes_agent.cli.auth.dingtalk import begin_registration, RegistrationError
|
|
|
|
with patch("hermes_agent.cli.auth.dingtalk._api_post",
|
|
return_value={"errcode": 0, "nonce": ""}):
|
|
with pytest.raises(RegistrationError, match="missing nonce"):
|
|
begin_registration()
|
|
|
|
def test_missing_device_code_raises(self):
|
|
from hermes_agent.cli.auth.dingtalk import begin_registration, RegistrationError
|
|
|
|
responses = [
|
|
{"errcode": 0, "nonce": "n1"},
|
|
{"errcode": 0, "verification_uri_complete": "http://x"}, # no device_code
|
|
]
|
|
with patch("hermes_agent.cli.auth.dingtalk._api_post", side_effect=responses):
|
|
with pytest.raises(RegistrationError, match="missing device_code"):
|
|
begin_registration()
|
|
|
|
def test_missing_verification_uri_raises(self):
|
|
from hermes_agent.cli.auth.dingtalk import begin_registration, RegistrationError
|
|
|
|
responses = [
|
|
{"errcode": 0, "nonce": "n1"},
|
|
{"errcode": 0, "device_code": "dev"}, # no verification_uri_complete
|
|
]
|
|
with patch("hermes_agent.cli.auth.dingtalk._api_post", side_effect=responses):
|
|
with pytest.raises(RegistrationError,
|
|
match="missing verification_uri_complete"):
|
|
begin_registration()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# wait_for_registration_success — polling loop
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestWaitForSuccess:
|
|
|
|
def test_returns_credentials_on_success(self):
|
|
from hermes_agent.cli.auth.dingtalk import wait_for_registration_success
|
|
|
|
responses = [
|
|
{"status": "WAITING"},
|
|
{"status": "WAITING"},
|
|
{"status": "SUCCESS", "client_id": "cid-1", "client_secret": "sec-1"},
|
|
]
|
|
with patch("hermes_agent.cli.auth.dingtalk.poll_registration", side_effect=responses), \
|
|
patch("hermes_agent.cli.auth.dingtalk.time.sleep"):
|
|
cid, secret = wait_for_registration_success(
|
|
device_code="dev", interval=0, expires_in=60
|
|
)
|
|
assert cid == "cid-1"
|
|
assert secret == "sec-1"
|
|
|
|
def test_success_without_credentials_raises(self):
|
|
from hermes_agent.cli.auth.dingtalk import wait_for_registration_success, RegistrationError
|
|
|
|
with patch("hermes_agent.cli.auth.dingtalk.poll_registration",
|
|
return_value={"status": "SUCCESS", "client_id": "", "client_secret": ""}), \
|
|
patch("hermes_agent.cli.auth.dingtalk.time.sleep"):
|
|
with pytest.raises(RegistrationError, match="credentials are missing"):
|
|
wait_for_registration_success(
|
|
device_code="dev", interval=0, expires_in=60
|
|
)
|
|
|
|
def test_invokes_waiting_callback(self):
|
|
from hermes_agent.cli.auth.dingtalk import wait_for_registration_success
|
|
|
|
callback = MagicMock()
|
|
responses = [
|
|
{"status": "WAITING"},
|
|
{"status": "WAITING"},
|
|
{"status": "SUCCESS", "client_id": "cid", "client_secret": "sec"},
|
|
]
|
|
with patch("hermes_agent.cli.auth.dingtalk.poll_registration", side_effect=responses), \
|
|
patch("hermes_agent.cli.auth.dingtalk.time.sleep"):
|
|
wait_for_registration_success(
|
|
device_code="dev", interval=0, expires_in=60, on_waiting=callback
|
|
)
|
|
assert callback.call_count == 2
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# QR rendering — terminal output
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRenderQR:
|
|
|
|
def test_returns_false_when_qrcode_missing(self, monkeypatch):
|
|
from hermes_agent.cli.auth import dingtalk as dingtalk_auth
|
|
|
|
# Simulate qrcode import failure
|
|
monkeypatch.setitem(sys.modules, "qrcode", None)
|
|
assert dingtalk_auth.render_qr_to_terminal("https://example.com") is False
|
|
|
|
def test_prints_when_qrcode_available(self, capsys):
|
|
"""End-to-end: render a real QR and verify SOMETHING got printed."""
|
|
try:
|
|
import qrcode # noqa: F401
|
|
except ImportError:
|
|
pytest.skip("qrcode library not available")
|
|
|
|
from hermes_agent.cli.auth.dingtalk import render_qr_to_terminal
|
|
result = render_qr_to_terminal("https://example.com/test")
|
|
captured = capsys.readouterr()
|
|
assert result is True
|
|
assert len(captured.out) > 100 # rendered matrix is non-trivial
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configuration — env var overrides
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestConfigOverrides:
|
|
|
|
def test_base_url_default(self, monkeypatch):
|
|
monkeypatch.delenv("DINGTALK_REGISTRATION_BASE_URL", raising=False)
|
|
# Force module reload to pick up current env
|
|
import importlib
|
|
import hermes_agent.cli.auth.dingtalk as mod
|
|
importlib.reload(mod)
|
|
assert mod.REGISTRATION_BASE_URL == "https://oapi.dingtalk.com"
|
|
|
|
def test_base_url_override_via_env(self, monkeypatch):
|
|
monkeypatch.setenv("DINGTALK_REGISTRATION_BASE_URL",
|
|
"https://test.example.com/")
|
|
import importlib
|
|
import hermes_agent.cli.auth.dingtalk as mod
|
|
importlib.reload(mod)
|
|
# Trailing slash stripped
|
|
assert mod.REGISTRATION_BASE_URL == "https://test.example.com"
|
|
|
|
def test_source_default(self, monkeypatch):
|
|
monkeypatch.delenv("DINGTALK_REGISTRATION_SOURCE", raising=False)
|
|
import importlib
|
|
import hermes_agent.cli.auth.dingtalk as mod
|
|
importlib.reload(mod)
|
|
assert mod.REGISTRATION_SOURCE == "openClaw"
|