Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,398 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.config import GatewayConfig, Platform, PlatformConfig
|
||||
from gateway.platforms.base import MessageEvent
|
||||
from gateway.session import SessionSource
|
||||
|
||||
|
||||
def _clear_auth_env(monkeypatch) -> None:
|
||||
for key in (
|
||||
"TELEGRAM_ALLOWED_USERS",
|
||||
"TELEGRAM_GROUP_ALLOWED_USERS",
|
||||
"DISCORD_ALLOWED_USERS",
|
||||
"WHATSAPP_ALLOWED_USERS",
|
||||
"SLACK_ALLOWED_USERS",
|
||||
"SIGNAL_ALLOWED_USERS",
|
||||
"SIGNAL_GROUP_ALLOWED_USERS",
|
||||
"TELEGRAM_GROUP_ALLOWED_CHATS",
|
||||
"EMAIL_ALLOWED_USERS",
|
||||
"SMS_ALLOWED_USERS",
|
||||
"MATTERMOST_ALLOWED_USERS",
|
||||
"MATRIX_ALLOWED_USERS",
|
||||
"DINGTALK_ALLOWED_USERS", "FEISHU_ALLOWED_USERS", "WECOM_ALLOWED_USERS",
|
||||
"QQ_ALLOWED_USERS", "QQ_GROUP_ALLOWED_USERS",
|
||||
"GATEWAY_ALLOWED_USERS",
|
||||
"TELEGRAM_ALLOW_ALL_USERS",
|
||||
"DISCORD_ALLOW_ALL_USERS",
|
||||
"WHATSAPP_ALLOW_ALL_USERS",
|
||||
"SLACK_ALLOW_ALL_USERS",
|
||||
"SIGNAL_ALLOW_ALL_USERS",
|
||||
"EMAIL_ALLOW_ALL_USERS",
|
||||
"SMS_ALLOW_ALL_USERS",
|
||||
"MATTERMOST_ALLOW_ALL_USERS",
|
||||
"MATRIX_ALLOW_ALL_USERS",
|
||||
"DINGTALK_ALLOW_ALL_USERS", "FEISHU_ALLOW_ALL_USERS", "WECOM_ALLOW_ALL_USERS",
|
||||
"QQ_ALLOW_ALL_USERS",
|
||||
"GATEWAY_ALLOW_ALL_USERS",
|
||||
):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
|
||||
def _make_event(
|
||||
platform: Platform,
|
||||
user_id: str,
|
||||
chat_id: str,
|
||||
*,
|
||||
profile: str | None = None,
|
||||
) -> MessageEvent:
|
||||
return MessageEvent(
|
||||
text="hello",
|
||||
message_id="m1",
|
||||
source=SessionSource(
|
||||
platform=platform,
|
||||
user_id=user_id,
|
||||
chat_id=chat_id,
|
||||
user_name="tester",
|
||||
chat_type="dm",
|
||||
profile=profile,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _make_runner(platform: Platform, config: GatewayConfig):
|
||||
from gateway.run import GatewayRunner
|
||||
|
||||
runner = object.__new__(GatewayRunner)
|
||||
runner.config = config
|
||||
adapter = SimpleNamespace(send=AsyncMock())
|
||||
runner.adapters = {platform: adapter}
|
||||
runner.pairing_store = MagicMock()
|
||||
runner.pairing_store.is_approved.return_value = False
|
||||
runner.pairing_store._is_rate_limited.return_value = False
|
||||
# Attributes required by _handle_message for the authorized-user path
|
||||
runner._running_agents = {}
|
||||
runner._running_agents_ts = {}
|
||||
runner._update_prompts = {}
|
||||
runner.hooks = SimpleNamespace(dispatch=AsyncMock(return_value=None))
|
||||
runner._sessions = {}
|
||||
return runner, adapter
|
||||
|
||||
|
||||
def test_whatsapp_lid_user_matches_phone_allowlist_via_modern_session_mapping(
|
||||
monkeypatch, tmp_path,
|
||||
):
|
||||
"""Modern ``platforms/`` installs store bridge mappings under
|
||||
``platforms/whatsapp/session`` — the LID→phone resolution (and therefore
|
||||
the allowlist match) must work there too, not just the legacy layout.
|
||||
Regression guard for the silently-dropped-LID-sender bug (#36664)."""
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.setenv("WHATSAPP_ALLOWED_USERS", "15550000001")
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
session_dir = tmp_path / "platforms" / "whatsapp" / "session"
|
||||
session_dir.mkdir(parents=True)
|
||||
(session_dir / "lid-mapping-15550000001.json").write_text(
|
||||
'"900000000000001"', encoding="utf-8",
|
||||
)
|
||||
(session_dir / "lid-mapping-900000000000001_reverse.json").write_text(
|
||||
'"15550000001"', encoding="utf-8",
|
||||
)
|
||||
|
||||
runner, _adapter = _make_runner(
|
||||
Platform.WHATSAPP,
|
||||
GatewayConfig(platforms={Platform.WHATSAPP: PlatformConfig(enabled=True)}),
|
||||
)
|
||||
|
||||
source = SessionSource(
|
||||
platform=Platform.WHATSAPP,
|
||||
user_id="900000000000001@lid",
|
||||
chat_id="900000000000001@lid",
|
||||
user_name="tester",
|
||||
chat_type="dm",
|
||||
)
|
||||
|
||||
assert runner._is_user_authorized(source) is True
|
||||
|
||||
|
||||
def test_simplex_allowlist_accepts_display_name(monkeypatch):
|
||||
"""SIMPLEX_ALLOWED_USERS should match the contact's display name as well
|
||||
as the numeric contactId. The SimpleX UI surfaces only display names, so
|
||||
operators naturally put those in the env var — and the adapter sets
|
||||
user_id=contactId for stability. Both forms must work. (#TBD)"""
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.delenv("SIMPLEX_ALLOWED_USERS", raising=False)
|
||||
monkeypatch.setenv("SIMPLEX_ALLOWED_USERS", "hujikuji")
|
||||
|
||||
# Register the simplex plugin so the env-var lookup resolves.
|
||||
from gateway.platform_registry import platform_registry, PlatformEntry
|
||||
platform_registry.register(PlatformEntry(
|
||||
name="simplex",
|
||||
label="SimpleX Chat",
|
||||
adapter_factory=lambda cfg: None,
|
||||
check_fn=lambda: True,
|
||||
allowed_users_env="SIMPLEX_ALLOWED_USERS",
|
||||
allow_all_env="SIMPLEX_ALLOW_ALL_USERS",
|
||||
))
|
||||
|
||||
simplex = Platform("simplex")
|
||||
runner, _adapter = _make_runner(
|
||||
simplex,
|
||||
GatewayConfig(platforms={simplex: PlatformConfig(enabled=True)}),
|
||||
)
|
||||
|
||||
# contactId in the allowlist would still work — but the operator chose
|
||||
# the display name. Verify the gateway honors it.
|
||||
source = SessionSource(
|
||||
platform=simplex,
|
||||
user_id="4", # adapter sets this to the numeric contactId
|
||||
chat_id="hujikuji",
|
||||
user_name="hujikuji", # adapter sets this to displayName
|
||||
chat_type="dm",
|
||||
)
|
||||
assert runner._is_user_authorized(source) is True
|
||||
|
||||
|
||||
def test_telegram_group_users_legacy_chat_ids_still_authorize(monkeypatch):
|
||||
"""Backward-compat: PR #15027 shipped TELEGRAM_GROUP_ALLOWED_USERS as a
|
||||
chat-ID allowlist. PR #17686 renamed it to sender IDs and added
|
||||
TELEGRAM_GROUP_ALLOWED_CHATS. Users on the old guidance must keep working:
|
||||
chat-ID-shaped values (starting with "-") in the _USERS var are honored as
|
||||
chat IDs with a deprecation warning.
|
||||
"""
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "-1001878443972")
|
||||
|
||||
runner, _adapter = _make_runner(
|
||||
Platform.TELEGRAM,
|
||||
GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}),
|
||||
)
|
||||
|
||||
source = SessionSource(
|
||||
platform=Platform.TELEGRAM,
|
||||
user_id="999",
|
||||
chat_id="-1001878443972",
|
||||
user_name="tester",
|
||||
chat_type="forum",
|
||||
)
|
||||
|
||||
assert runner._is_user_authorized(source) is True
|
||||
|
||||
|
||||
def test_telegram_group_users_mixed_sender_and_legacy_chat(monkeypatch):
|
||||
"""Mixed values: positive user ID gates senders; negative chat ID gates chat."""
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "999,-1001878443972")
|
||||
|
||||
runner, _adapter = _make_runner(
|
||||
Platform.TELEGRAM,
|
||||
GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}),
|
||||
)
|
||||
|
||||
# Legacy chat ID path: any sender in the listed chat is authorized
|
||||
legacy_chat_source = SessionSource(
|
||||
platform=Platform.TELEGRAM,
|
||||
user_id="123",
|
||||
chat_id="-1001878443972",
|
||||
user_name="tester",
|
||||
chat_type="group",
|
||||
)
|
||||
assert runner._is_user_authorized(legacy_chat_source) is True
|
||||
|
||||
# Sender path: listed sender user ID authorized in any group
|
||||
sender_source = SessionSource(
|
||||
platform=Platform.TELEGRAM,
|
||||
user_id="999",
|
||||
chat_id="-1009999999999",
|
||||
user_name="tester",
|
||||
chat_type="group",
|
||||
)
|
||||
assert runner._is_user_authorized(sender_source) is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unauthorized_dm_pairs_by_default(monkeypatch):
|
||||
_clear_auth_env(monkeypatch)
|
||||
config = GatewayConfig(
|
||||
platforms={Platform.WHATSAPP: PlatformConfig(enabled=True)},
|
||||
)
|
||||
runner, adapter = _make_runner(Platform.WHATSAPP, config)
|
||||
runner.pairing_store.generate_code.return_value = "ABC12DEF"
|
||||
|
||||
result = await runner._handle_message(
|
||||
_make_event(
|
||||
Platform.WHATSAPP,
|
||||
"15551234567@s.whatsapp.net",
|
||||
"15551234567@s.whatsapp.net",
|
||||
)
|
||||
)
|
||||
|
||||
assert result is None
|
||||
runner.pairing_store.generate_code.assert_called_once_with(
|
||||
"whatsapp",
|
||||
"15551234567@s.whatsapp.net",
|
||||
"tester",
|
||||
)
|
||||
adapter.send.assert_awaited_once()
|
||||
assert "ABC12DEF" in adapter.send.await_args.args[1]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unauthorized_whatsapp_dm_can_be_ignored(monkeypatch):
|
||||
_clear_auth_env(monkeypatch)
|
||||
config = GatewayConfig(
|
||||
platforms={
|
||||
Platform.WHATSAPP: PlatformConfig(
|
||||
enabled=True,
|
||||
extra={"unauthorized_dm_behavior": "ignore"},
|
||||
),
|
||||
},
|
||||
)
|
||||
runner, adapter = _make_runner(Platform.WHATSAPP, config)
|
||||
|
||||
result = await runner._handle_message(
|
||||
_make_event(
|
||||
Platform.WHATSAPP,
|
||||
"15551234567@s.whatsapp.net",
|
||||
"15551234567@s.whatsapp.net",
|
||||
)
|
||||
)
|
||||
|
||||
assert result is None
|
||||
runner.pairing_store.generate_code.assert_not_called()
|
||||
adapter.send.assert_not_awaited()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Allowlist-configured platforms default to "ignore" for unauthorized users
|
||||
# (#9337: Signal gateway sends pairing spam when allowlist is configured)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_signal_with_allowlist_ignores_unauthorized_dm(monkeypatch):
|
||||
"""When SIGNAL_ALLOWED_USERS is set, unauthorized DMs are silently dropped.
|
||||
|
||||
This is the primary regression test for #9337: before the fix, Signal
|
||||
would send pairing codes to ANY sender even when a strict allowlist was
|
||||
configured, spamming personal contacts with cryptic bot messages.
|
||||
"""
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.setenv("SIGNAL_ALLOWED_USERS", "+15550000001") # allowlist set
|
||||
|
||||
config = GatewayConfig(
|
||||
platforms={Platform.SIGNAL: PlatformConfig(enabled=True)},
|
||||
)
|
||||
runner, adapter = _make_runner(Platform.SIGNAL, config)
|
||||
|
||||
result = await runner._handle_message(
|
||||
_make_event(Platform.SIGNAL, "+15559999999", "+15559999999") # not in allowlist
|
||||
)
|
||||
|
||||
assert result is None
|
||||
runner.pairing_store.generate_code.assert_not_called()
|
||||
adapter.send.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_telegram_with_allowlist_ignores_unauthorized_dm(monkeypatch):
|
||||
"""Same behavior for Telegram: allowlist ⟹ ignore unauthorized DMs."""
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "111111111")
|
||||
|
||||
config = GatewayConfig(
|
||||
platforms={Platform.TELEGRAM: PlatformConfig(enabled=True)},
|
||||
)
|
||||
runner, adapter = _make_runner(Platform.TELEGRAM, config)
|
||||
|
||||
result = await runner._handle_message(
|
||||
_make_event(Platform.TELEGRAM, "999999999", "999999999")
|
||||
)
|
||||
|
||||
assert result is None
|
||||
runner.pairing_store.generate_code.assert_not_called()
|
||||
adapter.send.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_global_allowlist_ignores_unauthorized_dm(monkeypatch):
|
||||
"""GATEWAY_ALLOWED_USERS also triggers the 'ignore' behavior."""
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.setenv("GATEWAY_ALLOWED_USERS", "111111111")
|
||||
|
||||
config = GatewayConfig(
|
||||
platforms={Platform.SIGNAL: PlatformConfig(enabled=True)},
|
||||
)
|
||||
runner, adapter = _make_runner(Platform.SIGNAL, config)
|
||||
|
||||
result = await runner._handle_message(
|
||||
_make_event(Platform.SIGNAL, "+15559999999", "+15559999999")
|
||||
)
|
||||
|
||||
assert result is None
|
||||
runner.pairing_store.generate_code.assert_not_called()
|
||||
adapter.send.assert_not_awaited()
|
||||
|
||||
|
||||
def test_allowlist_authorized_user_returns_ignore_for_unauthorized(monkeypatch):
|
||||
"""_get_unauthorized_dm_behavior returns 'ignore' when allowlist is set.
|
||||
|
||||
We test the resolver directly. The full _handle_message path for
|
||||
authorized users is covered by the integration tests in this module.
|
||||
"""
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.setenv("SIGNAL_ALLOWED_USERS", "+15550000001")
|
||||
|
||||
config = GatewayConfig(
|
||||
platforms={Platform.SIGNAL: PlatformConfig(enabled=True)},
|
||||
)
|
||||
runner, _adapter = _make_runner(Platform.SIGNAL, config)
|
||||
|
||||
behavior = runner._get_unauthorized_dm_behavior(Platform.SIGNAL)
|
||||
assert behavior == "ignore"
|
||||
|
||||
|
||||
def test_get_unauthorized_dm_behavior_no_allowlist_returns_pair(monkeypatch):
|
||||
"""Without any allowlist, 'pair' is still the default."""
|
||||
_clear_auth_env(monkeypatch)
|
||||
|
||||
config = GatewayConfig(
|
||||
platforms={Platform.SIGNAL: PlatformConfig(enabled=True)},
|
||||
)
|
||||
runner, _adapter = _make_runner(Platform.SIGNAL, config)
|
||||
|
||||
behavior = runner._get_unauthorized_dm_behavior(Platform.SIGNAL)
|
||||
assert behavior == "pair"
|
||||
|
||||
|
||||
def test_get_unauthorized_dm_behavior_email_no_allowlist_returns_ignore(monkeypatch):
|
||||
_clear_auth_env(monkeypatch)
|
||||
|
||||
config = GatewayConfig(
|
||||
platforms={Platform.EMAIL: PlatformConfig(enabled=True)},
|
||||
)
|
||||
runner, _adapter = _make_runner(Platform.EMAIL, config)
|
||||
|
||||
behavior = runner._get_unauthorized_dm_behavior(Platform.EMAIL)
|
||||
assert behavior == "ignore"
|
||||
|
||||
|
||||
def test_qqbot_with_allowlist_ignores_unauthorized_dm(monkeypatch):
|
||||
"""QQBOT is included in the allowlist-aware default (QQ_ALLOWED_USERS).
|
||||
|
||||
Regression guard: the initial #9337 fix omitted QQBOT from the env map
|
||||
inside _get_unauthorized_dm_behavior, even though _is_user_authorized
|
||||
mapped it to QQ_ALLOWED_USERS. Without QQBOT here, a QQ operator with a
|
||||
strict user allowlist would still get pairing codes sent to strangers.
|
||||
"""
|
||||
_clear_auth_env(monkeypatch)
|
||||
monkeypatch.setenv("QQ_ALLOWED_USERS", "allowed-openid-1")
|
||||
|
||||
config = GatewayConfig(
|
||||
platforms={Platform.QQBOT: PlatformConfig(enabled=True)},
|
||||
)
|
||||
runner, _adapter = _make_runner(Platform.QQBOT, config)
|
||||
|
||||
behavior = runner._get_unauthorized_dm_behavior(Platform.QQBOT)
|
||||
assert behavior == "ignore"
|
||||
Reference in New Issue
Block a user