test_unauthorized_dm_behavior.py
1 from types import SimpleNamespace 2 from unittest.mock import AsyncMock, MagicMock 3 4 import pytest 5 6 from gateway.config import GatewayConfig, Platform, PlatformConfig 7 from gateway.platforms.base import MessageEvent 8 from gateway.session import SessionSource 9 10 11 def _clear_auth_env(monkeypatch) -> None: 12 for key in ( 13 "TELEGRAM_ALLOWED_USERS", 14 "TELEGRAM_GROUP_ALLOWED_USERS", 15 "DISCORD_ALLOWED_USERS", 16 "WHATSAPP_ALLOWED_USERS", 17 "SLACK_ALLOWED_USERS", 18 "SIGNAL_ALLOWED_USERS", 19 "SIGNAL_GROUP_ALLOWED_USERS", 20 "TELEGRAM_GROUP_ALLOWED_CHATS", 21 "EMAIL_ALLOWED_USERS", 22 "SMS_ALLOWED_USERS", 23 "MATTERMOST_ALLOWED_USERS", 24 "MATRIX_ALLOWED_USERS", 25 "DINGTALK_ALLOWED_USERS", "FEISHU_ALLOWED_USERS", "WECOM_ALLOWED_USERS", 26 "QQ_ALLOWED_USERS", "QQ_GROUP_ALLOWED_USERS", 27 "GATEWAY_ALLOWED_USERS", 28 "TELEGRAM_ALLOW_ALL_USERS", 29 "DISCORD_ALLOW_ALL_USERS", 30 "WHATSAPP_ALLOW_ALL_USERS", 31 "SLACK_ALLOW_ALL_USERS", 32 "SIGNAL_ALLOW_ALL_USERS", 33 "EMAIL_ALLOW_ALL_USERS", 34 "SMS_ALLOW_ALL_USERS", 35 "MATTERMOST_ALLOW_ALL_USERS", 36 "MATRIX_ALLOW_ALL_USERS", 37 "DINGTALK_ALLOW_ALL_USERS", "FEISHU_ALLOW_ALL_USERS", "WECOM_ALLOW_ALL_USERS", 38 "QQ_ALLOW_ALL_USERS", 39 "GATEWAY_ALLOW_ALL_USERS", 40 ): 41 monkeypatch.delenv(key, raising=False) 42 43 44 def _make_event(platform: Platform, user_id: str, chat_id: str) -> MessageEvent: 45 return MessageEvent( 46 text="hello", 47 message_id="m1", 48 source=SessionSource( 49 platform=platform, 50 user_id=user_id, 51 chat_id=chat_id, 52 user_name="tester", 53 chat_type="dm", 54 ), 55 ) 56 57 58 def _make_runner(platform: Platform, config: GatewayConfig): 59 from gateway.run import GatewayRunner 60 61 runner = object.__new__(GatewayRunner) 62 runner.config = config 63 adapter = SimpleNamespace(send=AsyncMock()) 64 runner.adapters = {platform: adapter} 65 runner.pairing_store = MagicMock() 66 runner.pairing_store.is_approved.return_value = False 67 runner.pairing_store._is_rate_limited.return_value = False 68 # Attributes required by _handle_message for the authorized-user path 69 runner._running_agents = {} 70 runner._running_agents_ts = {} 71 runner._update_prompts = {} 72 runner.hooks = SimpleNamespace(dispatch=AsyncMock(return_value=None)) 73 runner._sessions = {} 74 return runner, adapter 75 76 77 def test_whatsapp_lid_user_matches_phone_allowlist_via_session_mapping(monkeypatch, tmp_path): 78 _clear_auth_env(monkeypatch) 79 monkeypatch.setenv("WHATSAPP_ALLOWED_USERS", "15550000001") 80 monkeypatch.setenv("HERMES_HOME", str(tmp_path)) 81 82 session_dir = tmp_path / "whatsapp" / "session" 83 session_dir.mkdir(parents=True) 84 (session_dir / "lid-mapping-15550000001.json").write_text('"900000000000001"', encoding="utf-8") 85 (session_dir / "lid-mapping-900000000000001_reverse.json").write_text('"15550000001"', encoding="utf-8") 86 87 runner, _adapter = _make_runner( 88 Platform.WHATSAPP, 89 GatewayConfig(platforms={Platform.WHATSAPP: PlatformConfig(enabled=True)}), 90 ) 91 92 source = SessionSource( 93 platform=Platform.WHATSAPP, 94 user_id="900000000000001@lid", 95 chat_id="900000000000001@lid", 96 user_name="tester", 97 chat_type="dm", 98 ) 99 100 assert runner._is_user_authorized(source) is True 101 102 103 def test_star_wildcard_in_allowlist_authorizes_any_user(monkeypatch): 104 """WHATSAPP_ALLOWED_USERS=* should act as allow-all wildcard.""" 105 _clear_auth_env(monkeypatch) 106 monkeypatch.setenv("WHATSAPP_ALLOWED_USERS", "*") 107 108 runner, _adapter = _make_runner( 109 Platform.WHATSAPP, 110 GatewayConfig(platforms={Platform.WHATSAPP: PlatformConfig(enabled=True)}), 111 ) 112 113 source = SessionSource( 114 platform=Platform.WHATSAPP, 115 user_id="99998887776@s.whatsapp.net", 116 chat_id="99998887776@s.whatsapp.net", 117 user_name="stranger", 118 chat_type="dm", 119 ) 120 assert runner._is_user_authorized(source) is True 121 122 123 def test_star_wildcard_works_for_any_platform(monkeypatch): 124 """The * wildcard should work generically, not just for WhatsApp.""" 125 _clear_auth_env(monkeypatch) 126 monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "*") 127 128 runner, _adapter = _make_runner( 129 Platform.TELEGRAM, 130 GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}), 131 ) 132 133 source = SessionSource( 134 platform=Platform.TELEGRAM, 135 user_id="123456789", 136 chat_id="123456789", 137 user_name="stranger", 138 chat_type="dm", 139 ) 140 assert runner._is_user_authorized(source) is True 141 142 143 def test_qq_group_allowlist_authorizes_group_chat_without_user_allowlist(monkeypatch): 144 _clear_auth_env(monkeypatch) 145 monkeypatch.setenv("QQ_GROUP_ALLOWED_USERS", "group-openid-1") 146 147 runner, _adapter = _make_runner( 148 Platform.QQBOT, 149 GatewayConfig(platforms={Platform.QQBOT: PlatformConfig(enabled=True)}), 150 ) 151 152 source = SessionSource( 153 platform=Platform.QQBOT, 154 user_id="member-openid-999", 155 chat_id="group-openid-1", 156 user_name="tester", 157 chat_type="group", 158 ) 159 160 assert runner._is_user_authorized(source) is True 161 162 163 def test_qq_group_allowlist_does_not_authorize_other_groups(monkeypatch): 164 _clear_auth_env(monkeypatch) 165 monkeypatch.setenv("QQ_GROUP_ALLOWED_USERS", "group-openid-1") 166 167 runner, _adapter = _make_runner( 168 Platform.QQBOT, 169 GatewayConfig(platforms={Platform.QQBOT: PlatformConfig(enabled=True)}), 170 ) 171 172 source = SessionSource( 173 platform=Platform.QQBOT, 174 user_id="member-openid-999", 175 chat_id="group-openid-2", 176 user_name="tester", 177 chat_type="group", 178 ) 179 180 assert runner._is_user_authorized(source) is False 181 182 183 def test_telegram_group_user_allowlist_authorizes_forum_sender_without_dm_allowlist(monkeypatch): 184 _clear_auth_env(monkeypatch) 185 monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "999") 186 187 runner, _adapter = _make_runner( 188 Platform.TELEGRAM, 189 GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}), 190 ) 191 source = SessionSource( 192 platform=Platform.TELEGRAM, 193 user_id="999", 194 chat_id="-1001878443972", 195 user_name="tester", 196 chat_type="forum", 197 ) 198 199 assert runner._is_user_authorized(source) is True 200 201 202 def test_telegram_group_user_allowlist_rejects_other_senders(monkeypatch): 203 _clear_auth_env(monkeypatch) 204 monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "999") 205 206 runner, _adapter = _make_runner( 207 Platform.TELEGRAM, 208 GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}), 209 ) 210 source = SessionSource( 211 platform=Platform.TELEGRAM, 212 user_id="123", 213 chat_id="-1001878443972", 214 user_name="tester", 215 chat_type="group", 216 ) 217 218 assert runner._is_user_authorized(source) is False 219 220 221 def test_telegram_group_user_allowlist_wildcard_authorizes_any_sender(monkeypatch): 222 _clear_auth_env(monkeypatch) 223 monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "*") 224 225 runner, _adapter = _make_runner( 226 Platform.TELEGRAM, 227 GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}), 228 ) 229 source = SessionSource( 230 platform=Platform.TELEGRAM, 231 user_id="123", 232 chat_id="-1001878443972", 233 user_name="tester", 234 chat_type="group", 235 ) 236 237 assert runner._is_user_authorized(source) is True 238 239 240 def test_telegram_group_user_allowlist_does_not_authorize_dms(monkeypatch): 241 _clear_auth_env(monkeypatch) 242 monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "999") 243 244 runner, _adapter = _make_runner( 245 Platform.TELEGRAM, 246 GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}), 247 ) 248 source = SessionSource( 249 platform=Platform.TELEGRAM, 250 user_id="999", 251 chat_id="999", 252 user_name="tester", 253 chat_type="dm", 254 ) 255 256 assert runner._is_user_authorized(source) is False 257 258 259 def test_telegram_group_chat_allowlist_authorizes_group_chat_without_user_allowlist(monkeypatch): 260 _clear_auth_env(monkeypatch) 261 monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_CHATS", "-1001878443972") 262 263 runner, _adapter = _make_runner( 264 Platform.TELEGRAM, 265 GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}), 266 ) 267 268 source = SessionSource( 269 platform=Platform.TELEGRAM, 270 user_id="999", 271 chat_id="-1001878443972", 272 user_name="tester", 273 chat_type="forum", 274 ) 275 276 assert runner._is_user_authorized(source) is True 277 278 279 def test_telegram_group_users_legacy_chat_ids_still_authorize(monkeypatch): 280 """Backward-compat: PR #15027 shipped TELEGRAM_GROUP_ALLOWED_USERS as a 281 chat-ID allowlist. PR #17686 renamed it to sender IDs and added 282 TELEGRAM_GROUP_ALLOWED_CHATS. Users on the old guidance must keep working: 283 chat-ID-shaped values (starting with "-") in the _USERS var are honored as 284 chat IDs with a deprecation warning. 285 """ 286 _clear_auth_env(monkeypatch) 287 monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "-1001878443972") 288 289 runner, _adapter = _make_runner( 290 Platform.TELEGRAM, 291 GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}), 292 ) 293 294 source = SessionSource( 295 platform=Platform.TELEGRAM, 296 user_id="999", 297 chat_id="-1001878443972", 298 user_name="tester", 299 chat_type="forum", 300 ) 301 302 assert runner._is_user_authorized(source) is True 303 304 305 def test_telegram_group_users_legacy_does_not_cross_chats(monkeypatch): 306 """Legacy chat-ID value only authorizes the listed chat, not any group.""" 307 _clear_auth_env(monkeypatch) 308 monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "-1001878443972") 309 310 runner, _adapter = _make_runner( 311 Platform.TELEGRAM, 312 GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}), 313 ) 314 315 source = SessionSource( 316 platform=Platform.TELEGRAM, 317 user_id="999", 318 chat_id="-1009999999999", 319 user_name="tester", 320 chat_type="group", 321 ) 322 323 assert runner._is_user_authorized(source) is False 324 325 326 def test_telegram_group_users_mixed_sender_and_legacy_chat(monkeypatch): 327 """Mixed values: positive user ID gates senders; negative chat ID gates chat.""" 328 _clear_auth_env(monkeypatch) 329 monkeypatch.setenv("TELEGRAM_GROUP_ALLOWED_USERS", "999,-1001878443972") 330 331 runner, _adapter = _make_runner( 332 Platform.TELEGRAM, 333 GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}), 334 ) 335 336 # Legacy chat ID path: any sender in the listed chat is authorized 337 legacy_chat_source = SessionSource( 338 platform=Platform.TELEGRAM, 339 user_id="123", 340 chat_id="-1001878443972", 341 user_name="tester", 342 chat_type="group", 343 ) 344 assert runner._is_user_authorized(legacy_chat_source) is True 345 346 # Sender path: listed sender user ID authorized in any group 347 sender_source = SessionSource( 348 platform=Platform.TELEGRAM, 349 user_id="999", 350 chat_id="-1009999999999", 351 user_name="tester", 352 chat_type="group", 353 ) 354 assert runner._is_user_authorized(sender_source) is True 355 356 357 @pytest.mark.asyncio 358 async def test_unauthorized_dm_pairs_by_default(monkeypatch): 359 _clear_auth_env(monkeypatch) 360 config = GatewayConfig( 361 platforms={Platform.WHATSAPP: PlatformConfig(enabled=True)}, 362 ) 363 runner, adapter = _make_runner(Platform.WHATSAPP, config) 364 runner.pairing_store.generate_code.return_value = "ABC12DEF" 365 366 result = await runner._handle_message( 367 _make_event( 368 Platform.WHATSAPP, 369 "15551234567@s.whatsapp.net", 370 "15551234567@s.whatsapp.net", 371 ) 372 ) 373 374 assert result is None 375 runner.pairing_store.generate_code.assert_called_once_with( 376 "whatsapp", 377 "15551234567@s.whatsapp.net", 378 "tester", 379 ) 380 adapter.send.assert_awaited_once() 381 assert "ABC12DEF" in adapter.send.await_args.args[1] 382 383 384 @pytest.mark.asyncio 385 async def test_unauthorized_whatsapp_dm_can_be_ignored(monkeypatch): 386 _clear_auth_env(monkeypatch) 387 config = GatewayConfig( 388 platforms={ 389 Platform.WHATSAPP: PlatformConfig( 390 enabled=True, 391 extra={"unauthorized_dm_behavior": "ignore"}, 392 ), 393 }, 394 ) 395 runner, adapter = _make_runner(Platform.WHATSAPP, config) 396 397 result = await runner._handle_message( 398 _make_event( 399 Platform.WHATSAPP, 400 "15551234567@s.whatsapp.net", 401 "15551234567@s.whatsapp.net", 402 ) 403 ) 404 405 assert result is None 406 runner.pairing_store.generate_code.assert_not_called() 407 adapter.send.assert_not_awaited() 408 409 410 @pytest.mark.asyncio 411 async def test_rate_limited_user_gets_no_response(monkeypatch): 412 """When a user is already rate-limited, pairing messages are silently ignored.""" 413 _clear_auth_env(monkeypatch) 414 config = GatewayConfig( 415 platforms={Platform.WHATSAPP: PlatformConfig(enabled=True)}, 416 ) 417 runner, adapter = _make_runner(Platform.WHATSAPP, config) 418 runner.pairing_store._is_rate_limited.return_value = True 419 420 result = await runner._handle_message( 421 _make_event( 422 Platform.WHATSAPP, 423 "15551234567@s.whatsapp.net", 424 "15551234567@s.whatsapp.net", 425 ) 426 ) 427 428 assert result is None 429 runner.pairing_store.generate_code.assert_not_called() 430 adapter.send.assert_not_awaited() 431 432 433 @pytest.mark.asyncio 434 async def test_rejection_message_records_rate_limit(monkeypatch): 435 """After sending a 'too many requests' rejection, rate limit is recorded 436 so subsequent messages are silently ignored.""" 437 _clear_auth_env(monkeypatch) 438 config = GatewayConfig( 439 platforms={Platform.WHATSAPP: PlatformConfig(enabled=True)}, 440 ) 441 runner, adapter = _make_runner(Platform.WHATSAPP, config) 442 runner.pairing_store.generate_code.return_value = None # triggers rejection 443 444 result = await runner._handle_message( 445 _make_event( 446 Platform.WHATSAPP, 447 "15551234567@s.whatsapp.net", 448 "15551234567@s.whatsapp.net", 449 ) 450 ) 451 452 assert result is None 453 adapter.send.assert_awaited_once() 454 assert "Too many" in adapter.send.await_args.args[1] 455 runner.pairing_store._record_rate_limit.assert_called_once_with( 456 "whatsapp", "15551234567@s.whatsapp.net" 457 ) 458 459 460 @pytest.mark.asyncio 461 async def test_global_ignore_suppresses_pairing_reply(monkeypatch): 462 _clear_auth_env(monkeypatch) 463 config = GatewayConfig( 464 unauthorized_dm_behavior="ignore", 465 platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="***")}, 466 ) 467 runner, adapter = _make_runner(Platform.TELEGRAM, config) 468 469 result = await runner._handle_message( 470 _make_event( 471 Platform.TELEGRAM, 472 "12345", 473 "12345", 474 ) 475 ) 476 477 assert result is None 478 runner.pairing_store.generate_code.assert_not_called() 479 adapter.send.assert_not_awaited() 480 481 482 # --------------------------------------------------------------------------- 483 # Allowlist-configured platforms default to "ignore" for unauthorized users 484 # (#9337: Signal gateway sends pairing spam when allowlist is configured) 485 # --------------------------------------------------------------------------- 486 487 @pytest.mark.asyncio 488 async def test_signal_with_allowlist_ignores_unauthorized_dm(monkeypatch): 489 """When SIGNAL_ALLOWED_USERS is set, unauthorized DMs are silently dropped. 490 491 This is the primary regression test for #9337: before the fix, Signal 492 would send pairing codes to ANY sender even when a strict allowlist was 493 configured, spamming personal contacts with cryptic bot messages. 494 """ 495 _clear_auth_env(monkeypatch) 496 monkeypatch.setenv("SIGNAL_ALLOWED_USERS", "+15550000001") # allowlist set 497 498 config = GatewayConfig( 499 platforms={Platform.SIGNAL: PlatformConfig(enabled=True)}, 500 ) 501 runner, adapter = _make_runner(Platform.SIGNAL, config) 502 503 result = await runner._handle_message( 504 _make_event(Platform.SIGNAL, "+15559999999", "+15559999999") # not in allowlist 505 ) 506 507 assert result is None 508 runner.pairing_store.generate_code.assert_not_called() 509 adapter.send.assert_not_awaited() 510 511 512 @pytest.mark.asyncio 513 async def test_telegram_with_allowlist_ignores_unauthorized_dm(monkeypatch): 514 """Same behavior for Telegram: allowlist ⟹ ignore unauthorized DMs.""" 515 _clear_auth_env(monkeypatch) 516 monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "111111111") 517 518 config = GatewayConfig( 519 platforms={Platform.TELEGRAM: PlatformConfig(enabled=True)}, 520 ) 521 runner, adapter = _make_runner(Platform.TELEGRAM, config) 522 523 result = await runner._handle_message( 524 _make_event(Platform.TELEGRAM, "999999999", "999999999") 525 ) 526 527 assert result is None 528 runner.pairing_store.generate_code.assert_not_called() 529 adapter.send.assert_not_awaited() 530 531 532 @pytest.mark.asyncio 533 async def test_global_allowlist_ignores_unauthorized_dm(monkeypatch): 534 """GATEWAY_ALLOWED_USERS also triggers the 'ignore' behavior.""" 535 _clear_auth_env(monkeypatch) 536 monkeypatch.setenv("GATEWAY_ALLOWED_USERS", "111111111") 537 538 config = GatewayConfig( 539 platforms={Platform.SIGNAL: PlatformConfig(enabled=True)}, 540 ) 541 runner, adapter = _make_runner(Platform.SIGNAL, config) 542 543 result = await runner._handle_message( 544 _make_event(Platform.SIGNAL, "+15559999999", "+15559999999") 545 ) 546 547 assert result is None 548 runner.pairing_store.generate_code.assert_not_called() 549 adapter.send.assert_not_awaited() 550 551 552 @pytest.mark.asyncio 553 async def test_no_allowlist_still_pairs_by_default(monkeypatch): 554 """Without any allowlist, pairing behavior is preserved (open gateway).""" 555 _clear_auth_env(monkeypatch) 556 # No SIGNAL_ALLOWED_USERS, no GATEWAY_ALLOWED_USERS 557 558 config = GatewayConfig( 559 platforms={Platform.SIGNAL: PlatformConfig(enabled=True)}, 560 ) 561 runner, adapter = _make_runner(Platform.SIGNAL, config) 562 runner.pairing_store.generate_code.return_value = "PAIR1234" 563 564 result = await runner._handle_message( 565 _make_event(Platform.SIGNAL, "+15559999999", "+15559999999") 566 ) 567 568 assert result is None 569 runner.pairing_store.generate_code.assert_called_once() 570 adapter.send.assert_awaited_once() 571 assert "PAIR1234" in adapter.send.await_args.args[1] 572 573 574 def test_explicit_pair_config_overrides_allowlist_default(monkeypatch): 575 """Explicit unauthorized_dm_behavior='pair' overrides the allowlist default. 576 577 Operators can opt back in to pairing even with an allowlist by setting 578 unauthorized_dm_behavior: pair in their platform config. We test the 579 _get_unauthorized_dm_behavior resolver directly to avoid the full 580 _handle_message pipeline which requires extensive runner state. 581 """ 582 _clear_auth_env(monkeypatch) 583 monkeypatch.setenv("SIGNAL_ALLOWED_USERS", "+15550000001") 584 585 config = GatewayConfig( 586 platforms={ 587 Platform.SIGNAL: PlatformConfig( 588 enabled=True, 589 extra={"unauthorized_dm_behavior": "pair"}, # explicit override 590 ), 591 }, 592 ) 593 runner, _adapter = _make_runner(Platform.SIGNAL, config) 594 595 # The per-platform explicit config should beat the allowlist-derived default 596 behavior = runner._get_unauthorized_dm_behavior(Platform.SIGNAL) 597 assert behavior == "pair" 598 599 600 def test_allowlist_authorized_user_returns_ignore_for_unauthorized(monkeypatch): 601 """_get_unauthorized_dm_behavior returns 'ignore' when allowlist is set. 602 603 We test the resolver directly. The full _handle_message path for 604 authorized users is covered by the integration tests in this module. 605 """ 606 _clear_auth_env(monkeypatch) 607 monkeypatch.setenv("SIGNAL_ALLOWED_USERS", "+15550000001") 608 609 config = GatewayConfig( 610 platforms={Platform.SIGNAL: PlatformConfig(enabled=True)}, 611 ) 612 runner, _adapter = _make_runner(Platform.SIGNAL, config) 613 614 behavior = runner._get_unauthorized_dm_behavior(Platform.SIGNAL) 615 assert behavior == "ignore" 616 617 618 def test_get_unauthorized_dm_behavior_no_allowlist_returns_pair(monkeypatch): 619 """Without any allowlist, 'pair' is still the default.""" 620 _clear_auth_env(monkeypatch) 621 622 config = GatewayConfig( 623 platforms={Platform.SIGNAL: PlatformConfig(enabled=True)}, 624 ) 625 runner, _adapter = _make_runner(Platform.SIGNAL, config) 626 627 behavior = runner._get_unauthorized_dm_behavior(Platform.SIGNAL) 628 assert behavior == "pair" 629 630 631 def test_qqbot_with_allowlist_ignores_unauthorized_dm(monkeypatch): 632 """QQBOT is included in the allowlist-aware default (QQ_ALLOWED_USERS). 633 634 Regression guard: the initial #9337 fix omitted QQBOT from the env map 635 inside _get_unauthorized_dm_behavior, even though _is_user_authorized 636 mapped it to QQ_ALLOWED_USERS. Without QQBOT here, a QQ operator with a 637 strict user allowlist would still get pairing codes sent to strangers. 638 """ 639 _clear_auth_env(monkeypatch) 640 monkeypatch.setenv("QQ_ALLOWED_USERS", "allowed-openid-1") 641 642 config = GatewayConfig( 643 platforms={Platform.QQBOT: PlatformConfig(enabled=True)}, 644 ) 645 runner, _adapter = _make_runner(Platform.QQBOT, config) 646 647 behavior = runner._get_unauthorized_dm_behavior(Platform.QQBOT) 648 assert behavior == "ignore"