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.
190 lines
6.6 KiB
Python
190 lines
6.6 KiB
Python
"""Tests for Telegram adapter early authorization check.
|
|
|
|
Verifies that unauthorized users are blocked before any text batching,
|
|
event building, or response generation occurs.
|
|
"""
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from gateway.config import Platform, PlatformConfig
|
|
from gateway.platforms.base import MessageType
|
|
|
|
|
|
def _make_adapter(allow_from=None, allowed_chats=None, group_allowed_chats=None, callback_auth=None, **extra_overrides):
|
|
try:
|
|
from plugins.platforms.telegram.adapter import TelegramAdapter
|
|
except ModuleNotFoundError: # PR branch before Telegram plugin extraction
|
|
from gateway.platforms.telegram import TelegramAdapter
|
|
|
|
extra = {}
|
|
if allow_from is not None:
|
|
extra["allow_from"] = allow_from
|
|
if allowed_chats is not None:
|
|
extra["allowed_chats"] = allowed_chats
|
|
if group_allowed_chats is not None:
|
|
extra["group_allowed_chats"] = group_allowed_chats
|
|
extra.update(extra_overrides)
|
|
|
|
adapter = object.__new__(TelegramAdapter)
|
|
adapter.platform = Platform.TELEGRAM
|
|
adapter.config = PlatformConfig(enabled=True, token="fake-token", extra=extra)
|
|
adapter._bot = SimpleNamespace(id=999, username="test_bot")
|
|
adapter._message_handler = AsyncMock()
|
|
adapter._pending_text_batches = {}
|
|
adapter._pending_text_batch_tasks = {}
|
|
adapter._text_batch_delay_seconds = 0.01
|
|
adapter._text_batch_split_delay_seconds = 0.01
|
|
adapter._mention_patterns = adapter._compile_mention_patterns()
|
|
adapter._forum_lock = asyncio.Lock()
|
|
adapter._forum_command_registered = set()
|
|
adapter._active_sessions = {}
|
|
adapter._pending_messages = {}
|
|
if callback_auth is not None:
|
|
adapter._is_callback_user_authorized = callback_auth
|
|
return adapter
|
|
|
|
|
|
def _make_message(text="hello", *, from_user_id=111, chat_id=-100, chat_type="group"):
|
|
return SimpleNamespace(
|
|
message_id=42,
|
|
text=text,
|
|
caption=None,
|
|
entities=[],
|
|
caption_entities=[],
|
|
message_thread_id=None,
|
|
is_topic_message=False,
|
|
chat=SimpleNamespace(id=chat_id, type=chat_type, title="Test", is_forum=False),
|
|
from_user=SimpleNamespace(id=from_user_id, full_name="Test User", first_name="Test"),
|
|
reply_to_message=None,
|
|
date=None,
|
|
location=None,
|
|
photo=None,
|
|
video=None,
|
|
audio=None,
|
|
voice=None,
|
|
document=None,
|
|
sticker=None,
|
|
media_group_id=None,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unauthorized_user_blocked_before_event_building():
|
|
"""Unauthorized user's message should be blocked before _build_message_event."""
|
|
adapter = _make_adapter(group_allow_from=["222"]) # Only user 222 allowed in groups
|
|
|
|
build_called = False
|
|
original_build = adapter._build_message_event
|
|
|
|
def track_build(*a, **kw):
|
|
nonlocal build_called
|
|
build_called = True
|
|
return original_build(*a, **kw)
|
|
|
|
adapter._build_message_event = track_build
|
|
|
|
update = SimpleNamespace(
|
|
update_id=1,
|
|
message=_make_message(from_user_id=111, chat_type="group"), # User 111 NOT in group_allow_from
|
|
effective_message=None,
|
|
)
|
|
|
|
await adapter._handle_text_message(update, SimpleNamespace())
|
|
|
|
assert build_called is False, "build_message_event should not be called for unauthorized user"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_command_from_unauthorized_user_blocked():
|
|
"""Commands from unauthorized users should be blocked."""
|
|
adapter = _make_adapter(group_allow_from=["222"])
|
|
adapter.handle_message = AsyncMock()
|
|
|
|
update = SimpleNamespace(
|
|
update_id=1,
|
|
message=_make_message(text="/start", from_user_id=111, chat_type="group"),
|
|
effective_message=None,
|
|
)
|
|
|
|
await adapter._handle_command(update, SimpleNamespace())
|
|
|
|
adapter.handle_message.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_location_from_unauthorized_user_blocked():
|
|
"""Location messages from unauthorized users should be blocked."""
|
|
adapter = _make_adapter(group_allow_from=["222"])
|
|
|
|
msg = _make_message(from_user_id=111, chat_type="group")
|
|
msg.text = None
|
|
msg.location = SimpleNamespace(latitude=53.3498, longitude=-6.2603)
|
|
|
|
update = SimpleNamespace(
|
|
update_id=1,
|
|
message=msg,
|
|
effective_message=None,
|
|
)
|
|
|
|
# Should not raise — just silently return
|
|
await adapter._handle_location_message(update, SimpleNamespace())
|
|
|
|
|
|
def test_is_user_authorized_from_message_allow_from():
|
|
"""_is_user_authorized_from_message should respect adapter-level allow_from for DMs."""
|
|
adapter = _make_adapter(allow_from=["111", "222"])
|
|
|
|
msg = _make_message(from_user_id=111, chat_type="dm")
|
|
assert adapter._is_user_authorized_from_message(msg) is True
|
|
|
|
msg = _make_message(from_user_id=333, chat_type="dm")
|
|
assert adapter._is_user_authorized_from_message(msg) is False
|
|
|
|
|
|
def test_runner_auth_gets_group_user_allowlist_context(monkeypatch):
|
|
"""Group user allowlists need a group-shaped source, not a DM-shaped one."""
|
|
monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "111")
|
|
seen_sources = []
|
|
|
|
class Runner:
|
|
def _is_user_authorized(self, source):
|
|
seen_sources.append(source)
|
|
return source.chat_type == "group" and source.chat_id == "-100" and source.user_id == "111"
|
|
|
|
async def handle(self, event):
|
|
return None
|
|
|
|
runner = Runner()
|
|
adapter = _make_adapter()
|
|
adapter._message_handler = runner.handle
|
|
msg = _make_message(from_user_id=111, chat_id=-100, chat_type="group")
|
|
|
|
assert adapter._is_user_authorized_from_message(msg) is True
|
|
assert seen_sources
|
|
assert seen_sources[0].chat_type == "group"
|
|
assert seen_sources[0].chat_id == "-100"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unmentioned_group_location_from_removed_user_not_observed():
|
|
"""Removed users must not persist unmentioned group locations into observed context."""
|
|
adapter = _make_adapter(
|
|
group_allow_from=["222"],
|
|
allowed_chats=["-100"],
|
|
group_allowed_chats=["-100"],
|
|
require_mention=True,
|
|
observe_unmentioned_group_messages=True,
|
|
)
|
|
observed = []
|
|
adapter._observe_unmentioned_group_message = lambda *args, **kwargs: observed.append((args, kwargs))
|
|
|
|
msg = _make_message(text=None, from_user_id=111, chat_id=-100, chat_type="group")
|
|
msg.location = SimpleNamespace(latitude=53.3498, longitude=-6.2603)
|
|
update = SimpleNamespace(update_id=1, message=msg, effective_message=None)
|
|
|
|
await adapter._handle_location_message(update, SimpleNamespace())
|
|
|
|
assert observed == []
|