hermes-agent/tests/gateway/test_telegram_auth_check.py
Teknium 39975613b1
test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
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.
2026-07-29 13:39:40 -07:00

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 == []