deer-flow/backend/tests/test_allowed_users.py
Zhengxu (Joshua) Jin 47b6755ce9
fix(qq): parse allowed_users with the shared fail-closed allowlist (#6730)
* fix(qq): parse allowed_users with the shared fail-closed allowlist

QQChannel built its allowlist with set(config.get("allowed_users") or []).
A single OpenID, which is what an `allowed_users: $QQ_ALLOWED_USERS`
reference resolves to, became a set of its characters, so the configured
user was silently dropped. A mapping authorized every key, including one
mapped to false, and a numeric scalar raised TypeError while the channel
was being constructed.

Parse it with the shared parse_allowed_users helper that Telegram and
WeChat already use: unset, null, empty collections and a blank string
allow everyone; a scalar OpenID is one entry; entries that are not
non-blank strings (such as numeric QQ account numbers) are dropped with a
warning, and a mapping is rejected as a whole; a configured value with no
valid OpenID denies every sender and logs an error. A scalar containing commas or whitespace warns
that it stays one literal ID. Binding codes still precede the allowlist.

* test(qq): gate group messages by member OpenID in allowlist tests

Every allowlist test sent a C2C frame, and the shared event() helper puts
the sender in both user_openid and member_openid, so a group-path
regression in sender resolution would pass unnoticed. admits() now takes
a kind; group frames carry only member_openid and a group_openid that
differs from the sender. The listed-OpenID test runs for both kinds, and a
new test shows that listing the group's OpenID does not admit its members.

Document that a blank or whitespace-only allowed_users string allows every
sender, like an unset value or an empty list.

* refactor(channels): share scalar allowlist separator warnings

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-12 22:19:11 +08:00

84 lines
2.5 KiB
Python

"""Shared allowlist warnings preserve literal IDs and channel opt-in."""
import logging
from typing import Any
import pytest
from app.channels.allowed_users import parse_allowed_users
logger = logging.getLogger(__name__)
def parse_string_id(entry: Any) -> str | None:
if isinstance(entry, str):
return entry.strip() or None
return None
@pytest.mark.parametrize("channel_name, valid_id_name", [("qq", "OpenID"), ("WeChat", "user ID")])
@pytest.mark.parametrize("separator", [",", ", ", " ", "\t", "\n", "\u2003"])
def test_opted_in_scalar_separator_warning_preserves_literal_id(channel_name, valid_id_name, separator, caplog):
literal_id = f"alice{separator}bob"
with caplog.at_level(logging.WARNING, logger=logger.name):
result = parse_allowed_users(
literal_id,
parse_user_id=parse_string_id,
logger=logger,
channel_name=channel_name,
expected_id="a string ID",
valid_id_name=valid_id_name,
warn_on_scalar_separators=True,
)
assert result == frozenset({literal_id})
assert len(caplog.records) == 1
warning = caplog.records[0]
assert warning.levelno == logging.WARNING
assert f"[{channel_name}]" in warning.message
assert f"one literal {valid_id_name}" in warning.message
assert "YAML list" in warning.message
assert "alice" not in warning.message
assert "bob" not in warning.message
def test_scalar_separator_warning_is_opt_in(caplog):
result = parse_allowed_users(
"alice,bob",
parse_user_id=parse_string_id,
logger=logger,
channel_name="other",
expected_id="a string ID",
)
assert result == frozenset({"alice,bob"})
assert caplog.records == []
@pytest.mark.parametrize(
"allowed_users, expected",
[
("alice", frozenset({"alice"})),
(" \talice\n ", frozenset({"alice"})),
(" \t\n ", None),
(None, None),
([], None),
(["alice,bob"], frozenset({"alice,bob"})),
(("alice bob",), frozenset({"alice bob"})),
({"alice\tbob"}, frozenset({"alice\tbob"})),
(frozenset({"alice\nbob"}), frozenset({"alice\nbob"})),
],
)
def test_opted_in_warning_only_checks_scalar_separators(allowed_users, expected, caplog):
result = parse_allowed_users(
allowed_users,
parse_user_id=parse_string_id,
logger=logger,
channel_name="other",
expected_id="a string ID",
warn_on_scalar_separators=True,
)
assert result == expected
assert caplog.records == []