/ tests / gateway / test_unauthorized_dm_behavior.py
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"