mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
Second, deeper pass over tools/gateway/hermes_cli plus first pass over the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker, dashboard, conformance, monitoring, secret_sources, hermes_state, providers). Same rubric as wave 1 (AGENTS.md test policy); security, alternation/caching invariants, issue-number regressions, and E2E kept. Real test-quality fixes found and rooted out along the way: - tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls (DEFAULT_CONFIG smart-approval leaked in) — pinned approval mode=manual via autouse fixture: 17.4s → 0.4s. - test_model_switch_custom_providers.py / test_user_providers_model_switch.py silently probed live provider catalogs (~2s/test) — stubbed cached_provider_model_ids/provider_model_ids/fetch_api_models. - test_telegram_noise_filter.py: 15-platform copy-paste matrix over shared gateway.run logic → 3 representative platforms (55s → 3.9s). - test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on MagicMock agents — interrupt.side_effect now clears _running_agents (22s → 1.0s). - test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait 5s → 0.5s. - test_telegram_init_deadline.py: loop-block margin restored to 1.0s with rationale comment — the watchdog-dump assertion needs the loop blocked well past deadline+grace under parallel load (flaked once in the 40-worker verification run at a 0.2s margin). Verification: full hermetic suite via scripts/run_tests.sh — 2,438 files, 21,718 tests passed, 0 failed, 293.9s wall. Suite totals vs original baseline: 46,820 → 19,757 test functions (−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
237 lines
6.9 KiB
Python
237 lines
6.9 KiB
Python
"""Contract tests for the generic non-interactive (bearer-token) auth seam.
|
|
|
|
Covers Task 2.0a: the reusable token-auth capability in the dashboard auth
|
|
framework — NOT the drain plugin (that's 2.0b/2.1). Asserts the ABC capability
|
|
flag, the registry filter, bearer extraction, provider stacking (verify_token),
|
|
and the route-agnostic middleware seam's fail-closed / 503 / pass-through
|
|
behaviour.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from typing import Optional
|
|
|
|
import pytest
|
|
|
|
from hermes_cli.dashboard_auth import (
|
|
DashboardAuthProvider,
|
|
LoginStart,
|
|
Session,
|
|
TokenPrincipal,
|
|
clear_providers,
|
|
list_providers,
|
|
list_session_providers,
|
|
list_token_providers,
|
|
register_provider,
|
|
)
|
|
from hermes_cli.dashboard_auth.base import ProviderError
|
|
from hermes_cli.dashboard_auth import token_auth
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Test doubles
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
class _OAuthOnly(DashboardAuthProvider):
|
|
"""A pure interactive provider — never token-authable."""
|
|
|
|
name = "oauth-only"
|
|
display_name = "OAuth Only"
|
|
|
|
def start_login(self, *, redirect_uri):
|
|
return LoginStart(redirect_url="x", cookie_payload={})
|
|
|
|
def complete_login(self, *, code, state, code_verifier, redirect_uri):
|
|
return Session("u", "e", "n", "o", self.name, 0, "a", "r")
|
|
|
|
def verify_session(self, *, access_token):
|
|
return None
|
|
|
|
def refresh_session(self, *, refresh_token):
|
|
return Session("u", "e", "n", "o", self.name, 0, "a", "r")
|
|
|
|
def revoke_session(self, *, refresh_token):
|
|
return None
|
|
|
|
|
|
class _TokenProvider(_OAuthOnly):
|
|
"""A token provider that accepts exactly one secret."""
|
|
|
|
name = "tok"
|
|
display_name = "Token Provider"
|
|
supports_token = True
|
|
|
|
def __init__(self, *, secret: str = "good-secret", scopes=("drain",)):
|
|
self._secret = secret
|
|
self._scopes = tuple(scopes)
|
|
|
|
def verify_token(self, *, token: str) -> Optional[TokenPrincipal]:
|
|
if token == self._secret:
|
|
return TokenPrincipal(
|
|
principal=self.name, provider=self.name, scopes=self._scopes
|
|
)
|
|
return None
|
|
|
|
|
|
class _UnreachableTokenProvider(_OAuthOnly):
|
|
name = "tok-down"
|
|
display_name = "Unreachable Token Provider"
|
|
supports_token = True
|
|
|
|
def verify_token(self, *, token: str) -> Optional[TokenPrincipal]:
|
|
raise ProviderError("backing store down")
|
|
|
|
|
|
class _BuggyTokenProvider(_OAuthOnly):
|
|
name = "tok-buggy"
|
|
display_name = "Buggy Token Provider"
|
|
supports_token = True
|
|
|
|
def verify_token(self, *, token: str) -> Optional[TokenPrincipal]:
|
|
raise RuntimeError("kaboom")
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Fixtures
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolated_state():
|
|
clear_providers()
|
|
token_auth.clear_token_routes()
|
|
yield
|
|
clear_providers()
|
|
token_auth.clear_token_routes()
|
|
|
|
|
|
class _FakeURL:
|
|
def __init__(self, path):
|
|
self.path = path
|
|
|
|
|
|
class _FakeClient:
|
|
host = "1.2.3.4"
|
|
|
|
|
|
class _FakeRequest:
|
|
"""Minimal Request stand-in for the seam (no real Starlette needed)."""
|
|
|
|
def __init__(self, path="/api/gateway/drain", headers=None):
|
|
self.url = _FakeURL(path)
|
|
self.headers = headers or {}
|
|
self.client = _FakeClient()
|
|
|
|
class _State:
|
|
pass
|
|
|
|
self.state = _State()
|
|
|
|
|
|
def _run(coro):
|
|
return asyncio.run(coro)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# ABC + registry
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def test_oauth_provider_defaults_supports_token_false():
|
|
assert _OAuthOnly().supports_token is False
|
|
|
|
|
|
|
|
|
|
class _NonInteractiveProvider(_TokenProvider):
|
|
"""A token-only credential with no interactive session."""
|
|
|
|
name = "svc-cred"
|
|
display_name = "Service Credential"
|
|
supports_session = False
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Bearer extraction
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# authenticate_token (provider stacking)
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def test_authenticate_token_accepts_valid():
|
|
register_provider(_TokenProvider(secret="good-secret"))
|
|
req = _FakeRequest(headers={"authorization": "Bearer good-secret"})
|
|
principal, unreachable = token_auth.authenticate_token(req)
|
|
assert unreachable is None
|
|
assert principal is not None
|
|
assert principal.provider == "tok"
|
|
assert principal.scopes == ("drain",)
|
|
|
|
|
|
def test_authenticate_token_rejects_wrong_secret():
|
|
register_provider(_TokenProvider(secret="good-secret"))
|
|
req = _FakeRequest(headers={"authorization": "Bearer wrong"})
|
|
principal, unreachable = token_auth.authenticate_token(req)
|
|
assert principal is None
|
|
assert unreachable is None
|
|
|
|
|
|
def test_authenticate_token_stacks_first_match_wins():
|
|
register_provider(_TokenProvider(secret="aaa"))
|
|
second = _TokenProvider(secret="bbb")
|
|
second.name = "tok2"
|
|
register_provider(second)
|
|
req = _FakeRequest(headers={"authorization": "Bearer bbb"})
|
|
principal, _ = token_auth.authenticate_token(req)
|
|
assert principal is not None and principal.provider == "tok2"
|
|
|
|
|
|
def test_authenticate_token_unreachable_then_valid_provider_wins():
|
|
register_provider(_UnreachableTokenProvider())
|
|
register_provider(_TokenProvider(secret="good"))
|
|
req = _FakeRequest(headers={"authorization": "Bearer good"})
|
|
principal, unreachable = token_auth.authenticate_token(req)
|
|
# A later provider accepting the token beats the earlier outage.
|
|
assert principal is not None and principal.provider == "tok"
|
|
assert unreachable is None
|
|
|
|
|
|
def test_authenticate_token_buggy_provider_does_not_crash():
|
|
register_provider(_BuggyTokenProvider())
|
|
register_provider(_TokenProvider(secret="good"))
|
|
req = _FakeRequest(headers={"authorization": "Bearer good"})
|
|
principal, unreachable = token_auth.authenticate_token(req)
|
|
assert principal is not None and principal.provider == "tok"
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Middleware seam (route-agnostic)
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
async def _call_next_ok(request):
|
|
from fastapi.responses import JSONResponse
|
|
|
|
return JSONResponse({"ok": True}, status_code=200)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_seam_rejects_wrong_token_401():
|
|
register_provider(_TokenProvider(secret="good"))
|
|
token_auth.register_token_route("/api/gateway/drain")
|
|
req = _FakeRequest(
|
|
path="/api/gateway/drain", headers={"authorization": "Bearer bad"}
|
|
)
|
|
resp = _run(token_auth.token_auth_middleware(req, _call_next_ok))
|
|
assert resp.status_code == 401
|
|
|
|
|