hermes-agent/tests/gateway/test_platform_connected_checkers.py
arimu1 9e4b89857a fix(gateway): require usable API_SERVER_KEY to enroll the api_server platform at load time
Salvaged from PR #36180 (commits 68dfeb4b16 and 86f437509b by arimu1),
re-applied onto current main with the incidental black-reformat churn
stripped out (~1,700 lines -> the semantic change + tests).

Previously gateway/config.py enrolled the api_server platform on
`api_server_enabled or api_server_key`, so API_SERVER_ENABLED=true with
no key (or a weak/placeholder key) still loaded the platform: the
adapter is instantiated (ResponseStore/SQLite opened in __init__), the
reconnect watcher spins, and the startup guard refuses at connect() —
logging errors forever. Now the platform is enrolled only when
API_SERVER_KEY passes the same strength bar as the adapter's startup
guard (has_usable_secret, min_length=16), via a shared
_has_usable_api_server_key() helper.

The no-op `lambda cfg: True` connected-checker for API_SERVER is also
replaced with the same key check, so get_connected_platforms() only
reports the platform "up" when it could actually start.

Known limitation (intentionally out of scope): a YAML config with
`platforms.api_server.enabled: true` and no key still loads the
platform; this gate covers the env-override path only.

Dropped from the original PR: EMAIL/SMS checker additions (scope creep
beyond the PR title; absent on current main) and the wholesale black
reformat of gateway/config.py and tests.

Fixes #36111

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 11:54:05 -07:00

153 lines
5.6 KiB
Python

"""
Verify that every gateway platform — built-in and plugin — has a connection
checker so ``GatewayConfig.get_connected_platforms()`` doesn't silently drop
platforms with bespoke auth requirements.
"""
from unittest.mock import MagicMock
import pytest
from gateway.config import Platform, _PLATFORM_CONNECTED_CHECKERS, _BUILTIN_PLATFORM_VALUES
def test_all_builtins_have_checker_or_generic_token_path():
"""Every built-in Platform member must be reachable by either:
1. The generic ``config.token or config.api_key`` check, OR
2. A platform-specific entry in ``_PLATFORM_CONNECTED_CHECKERS``.
This guarantees ``get_connected_platforms()`` doesn't silently ignore
a built-in just because nobody added it to the checker dict.
"""
# Platforms covered by the generic token/api_key branch
generic_token_values = {p.value for p in {
Platform.TELEGRAM,
Platform.DISCORD,
Platform.SLACK,
Platform.MATRIX,
Platform.MATTERMOST,
Platform.HOMEASSISTANT,
}}
# Platforms with a bespoke checker
checker_values = {p.value for p in set(_PLATFORM_CONNECTED_CHECKERS.keys())}
# Platforms whose connection check now comes from a registered plugin entry
# (is_connected / validate_config). Several adapters migrated out of core
# into bundled plugins (#41112); their checker moved with them to the
# platform registry, so get_connected_platforms() resolves them via the
# registry fallback rather than _PLATFORM_CONNECTED_CHECKERS.
plugin_checker_values: set[str] = set()
try:
from hermes_cli.plugins import discover_plugins
from gateway.platform_registry import platform_registry
discover_plugins()
for _entry in platform_registry.all_entries():
if _entry.is_connected is not None or _entry.validate_config is not None:
plugin_checker_values.add(_entry.name)
except Exception:
pass
# Every built-in should be in one of the sets
all_builtins = set(_BUILTIN_PLATFORM_VALUES)
missing = (
all_builtins
- generic_token_values
- checker_values
- plugin_checker_values
- {"local"}
)
assert not missing, (
f"Built-in platforms missing a connection checker: "
f"{sorted(missing)}. "
f"Add them to _PLATFORM_CONNECTED_CHECKERS or generic_token_platforms."
)
@pytest.mark.parametrize("platform, checker", list(_PLATFORM_CONNECTED_CHECKERS.items()))
def test_checker_handles_minimal_config(platform, checker):
"""Each bespoke checker must not crash on a minimal PlatformConfig."""
mock_config = MagicMock()
mock_config.extra = {}
mock_config.token = None
mock_config.api_key = None
mock_config.enabled = True
# Should return a bool without raising
result = checker(mock_config)
assert isinstance(result, bool)
@pytest.mark.parametrize("platform, checker", list(_PLATFORM_CONNECTED_CHECKERS.items()))
def test_checker_returns_true_when_configured(platform, checker, monkeypatch):
"""Each bespoke checker must return True when the config looks valid."""
mock_config = MagicMock()
mock_config.token = None
mock_config.api_key = None
mock_config.enabled = True
# Set up platform-specific mock extra fields so the checker succeeds
if platform == Platform.WEIXIN:
mock_config.extra = {"account_id": "123", "token": "***"}
elif platform == Platform.SIGNAL:
mock_config.extra = {"http_url": "http://signal:8080"}
elif platform == Platform.EMAIL:
mock_config.extra = {"address": "hermes@example.com"}
elif platform == Platform.SMS:
monkeypatch.setenv("TWILIO_ACCOUNT_SID", "ACtest")
mock_config.extra = {}
elif platform == Platform.API_SERVER:
mock_config.extra = {"key": "opensslrandhex32strongkey"}
elif platform in {
Platform.WEBHOOK,
Platform.WHATSAPP,
}:
mock_config.extra = {}
elif platform == Platform.MSGRAPH_WEBHOOK:
mock_config.extra = {"client_state": "expected-client-state"}
elif platform == Platform.FEISHU:
mock_config.extra = {"app_id": "app"}
elif platform == Platform.WECOM:
mock_config.extra = {"bot_id": "bot"}
elif platform == Platform.WECOM_CALLBACK:
mock_config.extra = {"corp_id": "corp"}
elif platform == Platform.BLUEBUBBLES:
mock_config.extra = {"server_url": "http://bb:1234", "password": "pw"}
elif platform == Platform.QQBOT:
mock_config.extra = {"app_id": "app", "client_secret": "sec"}
elif platform == Platform.YUANBAO:
mock_config.extra = {"app_id": "app", "app_secret": "sec"}
elif platform == Platform.DINGTALK:
mock_config.extra = {"client_id": "id", "client_secret": "sec"}
elif platform == Platform.RELAY:
mock_config.extra = {"relay_url": "wss://connector.example/relay"}
else:
pytest.skip(f"No synthetic config defined for {platform.value}")
result = checker(mock_config)
assert result is True, f"{platform.value} checker should return True with valid-looking config"
def test_api_server_checker_key_validity():
"""API_SERVER checker: missing, placeholder, short, and strong keys."""
checker = _PLATFORM_CONNECTED_CHECKERS[Platform.API_SERVER]
cfg = MagicMock()
# Missing
cfg.extra = {}
assert checker(cfg) is False
# Placeholder
cfg.extra = {"key": "changeme"}
assert checker(cfg) is False
# Too short (<16 chars)
cfg.extra = {"key": "shortkey"}
assert checker(cfg) is False
# Strong key (>=16 chars, not a placeholder)
cfg.extra = {"key": "opensslrandhex32strongkey"}
assert checker(cfg) is True