Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
"""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_allowlist_dm_with_explicit_pair_behavior_reaches_gateway(monkeypatch):
|
||||
"""Allowlist + unauthorized_dm_behavior:pair must not early-drop unknown DMs.
|
||||
|
||||
Regression for the gap left by #40863: early intake rejection discarded
|
||||
unauthorized DMs before gateway pairing could run, even when the operator
|
||||
explicitly set telegram.unauthorized_dm_behavior: pair.
|
||||
"""
|
||||
monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "111")
|
||||
|
||||
class Runner:
|
||||
def _is_user_authorized(self, source):
|
||||
return source.user_id == "111"
|
||||
|
||||
def _get_unauthorized_dm_behavior(self, platform, *, profile=None):
|
||||
assert platform == Platform.TELEGRAM
|
||||
return "pair"
|
||||
|
||||
async def handle(self, event):
|
||||
return None
|
||||
|
||||
runner = Runner()
|
||||
adapter = _make_adapter()
|
||||
adapter._message_handler = runner.handle
|
||||
msg = _make_message(from_user_id=999, chat_id=999, chat_type="private")
|
||||
|
||||
assert adapter._is_user_authorized_from_message(msg) is True
|
||||
|
||||
|
||||
def test_allowlist_dm_without_pair_behavior_still_early_rejects(monkeypatch):
|
||||
"""Allowlist without pairing opt-in keeps the #9337/#40863 silent drop."""
|
||||
monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "111")
|
||||
|
||||
class Runner:
|
||||
def _is_user_authorized(self, source):
|
||||
return source.user_id == "111"
|
||||
|
||||
def _get_unauthorized_dm_behavior(self, platform, *, profile=None):
|
||||
return "ignore"
|
||||
|
||||
async def handle(self, event):
|
||||
return None
|
||||
|
||||
runner = Runner()
|
||||
adapter = _make_adapter()
|
||||
adapter._message_handler = runner.handle
|
||||
msg = _make_message(from_user_id=999, chat_id=999, chat_type="private")
|
||||
|
||||
assert adapter._is_user_authorized_from_message(msg) is False
|
||||
|
||||
|
||||
def test_allow_from_dm_with_pair_override_reaches_gateway():
|
||||
"""Adapter allow_from + unauthorized_dm_behavior:pair still forwards DMs."""
|
||||
adapter = _make_adapter(
|
||||
allow_from=["111"],
|
||||
unauthorized_dm_behavior="pair",
|
||||
)
|
||||
msg = _make_message(from_user_id=999, chat_id=999, chat_type="dm")
|
||||
assert adapter._is_user_authorized_from_message(msg) is True
|
||||
|
||||
|
||||
def test_allowlist_group_with_pair_behavior_still_early_rejects(monkeypatch):
|
||||
"""Pairing is DM-only — unauthorized group senders stay blocked early."""
|
||||
monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "111")
|
||||
|
||||
class Runner:
|
||||
def _is_user_authorized(self, source):
|
||||
return source.user_id == "111"
|
||||
|
||||
def _get_unauthorized_dm_behavior(self, platform, *, profile=None):
|
||||
return "pair"
|
||||
|
||||
async def handle(self, event):
|
||||
return None
|
||||
|
||||
runner = Runner()
|
||||
adapter = _make_adapter(group_allow_from=["111"])
|
||||
adapter._message_handler = runner.handle
|
||||
msg = _make_message(from_user_id=999, chat_id=-100, chat_type="group")
|
||||
|
||||
assert adapter._is_user_authorized_from_message(msg) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unauthorized_dm_with_pair_behavior_builds_event(monkeypatch):
|
||||
"""Unknown DM under pair behavior must reach event construction."""
|
||||
monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "111")
|
||||
|
||||
class Runner:
|
||||
def _is_user_authorized(self, source):
|
||||
return source.user_id == "111"
|
||||
|
||||
def _get_unauthorized_dm_behavior(self, platform, *, profile=None):
|
||||
return "pair"
|
||||
|
||||
async def handle(self, event):
|
||||
return None
|
||||
|
||||
runner = Runner()
|
||||
adapter = _make_adapter()
|
||||
adapter._message_handler = runner.handle
|
||||
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
|
||||
adapter._enqueue_text_event = lambda event: None
|
||||
adapter._ensure_forum_commands = AsyncMock()
|
||||
adapter._cache_replied_media = AsyncMock()
|
||||
adapter._apply_telegram_group_observe_attribution = lambda event: event
|
||||
adapter._clean_bot_trigger_text = lambda text: text
|
||||
adapter._should_process_message = lambda *a, **kw: True
|
||||
|
||||
update = SimpleNamespace(
|
||||
update_id=1,
|
||||
message=_make_message(from_user_id=999, chat_id=999, chat_type="private"),
|
||||
effective_message=None,
|
||||
)
|
||||
await adapter._handle_text_message(update, SimpleNamespace())
|
||||
assert build_called is True
|
||||
|
||||
|
||||
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 == []
|
||||
|
||||
|
||||
def test_group_allowlist_authorized_under_multiplex_closure_handler(monkeypatch):
|
||||
"""group_allowed_chats must authorize a chat member under multiplex_profiles.
|
||||
|
||||
Regression for #87132: with gateway.multiplex_profiles the primary message
|
||||
handler is a closure, so its ``__self__`` is absent and the early intake
|
||||
filter could not reach GatewayRunner._is_user_authorized — it fell back to
|
||||
env-only auth and default-denied every non-global sender in an explicitly
|
||||
allowlisted group. The platform-bound callback registered via
|
||||
set_authorization_check survives the closure wrapping and must be consulted.
|
||||
"""
|
||||
monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_CHATS", "-100123")
|
||||
|
||||
adapter = _make_adapter(group_allowed_chats=["-100123"])
|
||||
|
||||
# Multiplex: the primary handler is a closure with no ``__self__`` runner.
|
||||
def closure_handler(event):
|
||||
return None
|
||||
|
||||
adapter._message_handler = closure_handler
|
||||
assert getattr(closure_handler, "__self__", None) is None
|
||||
|
||||
# The runner installs this callback at adapter registration; it routes
|
||||
# through the full auth chain (here: the chat allowlist) regardless of how
|
||||
# the message handler is wrapped.
|
||||
def auth_check(user_id, chat_type=None, chat_id=None):
|
||||
return str(chat_id) in {"-100123"}
|
||||
|
||||
adapter.set_authorization_check(auth_check)
|
||||
|
||||
# A sender absent from any user allowlist, posting in the allowlisted group,
|
||||
# is authorized via the chat allowlist.
|
||||
allowed = _make_message(from_user_id=555, chat_id=-100123, chat_type="group")
|
||||
assert adapter._is_user_authorized_from_message(allowed) is True
|
||||
|
||||
# The same sender in a NON-allowlisted group is still rejected.
|
||||
denied = _make_message(from_user_id=555, chat_id=-100999, chat_type="group")
|
||||
assert adapter._is_user_authorized_from_message(denied) is False
|
||||
|
||||
|
||||
def test_multiplex_closure_handler_without_callback_falls_back_to_env(monkeypatch):
|
||||
"""No registered callback + a closure handler (no runner) must not raise and
|
||||
falls back to env-only auth — the getattr guard keeps the legacy path safe."""
|
||||
monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_CHATS", "-100123")
|
||||
monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "111")
|
||||
|
||||
adapter = _make_adapter(group_allowed_chats=["-100123"])
|
||||
adapter._message_handler = lambda event: None # closure, no __self__
|
||||
# No set_authorization_check() → _authorization_check is absent/None.
|
||||
|
||||
assert adapter._is_user_authorized_from_message(
|
||||
_make_message(from_user_id=111, chat_id=-100123, chat_type="group")
|
||||
) is True
|
||||
assert adapter._is_user_authorized_from_message(
|
||||
_make_message(from_user_id=555, chat_id=-100123, chat_type="group")
|
||||
) is False
|
||||
Reference in New Issue
Block a user