Files
wehub-resource-sync b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
Build Skills Index / trigger-deploy (push) Waiting to run
CI / Deny unrelated histories (push) Has been skipped
CI / CI timing report (push) Blocked by required conditions
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / All required checks pass (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 11:56:03 +08:00

218 lines
8.5 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_cli.dingtalk_auth import _api_post, RegistrationError
with patch("hermes_cli.dingtalk_auth.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_cli.dingtalk_auth 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_cli.dingtalk_auth.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_cli.dingtalk_auth import _api_post
mock_resp = MagicMock()
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = {"errcode": 0, "nonce": "abc"}
with patch("hermes_cli.dingtalk_auth.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_cli.dingtalk_auth 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_cli.dingtalk_auth._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_cli.dingtalk_auth import begin_registration, RegistrationError
with patch("hermes_cli.dingtalk_auth._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_cli.dingtalk_auth import begin_registration, RegistrationError
responses = [
{"errcode": 0, "nonce": "n1"},
{"errcode": 0, "verification_uri_complete": "http://x"}, # no device_code
]
with patch("hermes_cli.dingtalk_auth._api_post", side_effect=responses):
with pytest.raises(RegistrationError, match="missing device_code"):
begin_registration()
def test_missing_verification_uri_raises(self):
from hermes_cli.dingtalk_auth import begin_registration, RegistrationError
responses = [
{"errcode": 0, "nonce": "n1"},
{"errcode": 0, "device_code": "dev"}, # no verification_uri_complete
]
with patch("hermes_cli.dingtalk_auth._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_cli.dingtalk_auth import wait_for_registration_success
responses = [
{"status": "WAITING"},
{"status": "WAITING"},
{"status": "SUCCESS", "client_id": "cid-1", "client_secret": "sec-1"},
]
with patch("hermes_cli.dingtalk_auth.poll_registration", side_effect=responses), \
patch("hermes_cli.dingtalk_auth.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_cli.dingtalk_auth import wait_for_registration_success, RegistrationError
with patch("hermes_cli.dingtalk_auth.poll_registration",
return_value={"status": "SUCCESS", "client_id": "", "client_secret": ""}), \
patch("hermes_cli.dingtalk_auth.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_cli.dingtalk_auth import wait_for_registration_success
callback = MagicMock()
responses = [
{"status": "WAITING"},
{"status": "WAITING"},
{"status": "SUCCESS", "client_id": "cid", "client_secret": "sec"},
]
with patch("hermes_cli.dingtalk_auth.poll_registration", side_effect=responses), \
patch("hermes_cli.dingtalk_auth.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_cli import 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_cli.dingtalk_auth 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_cli.dingtalk_auth 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_cli.dingtalk_auth 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_cli.dingtalk_auth as mod
importlib.reload(mod)
assert mod.REGISTRATION_SOURCE == "openClaw"