fix(pairing): normalize sender IDs to str in the pairing store

This commit is contained in:
w.antar
2026-06-24 10:29:58 +08:00
committed by Xubin Ren
parent bc1df49201
commit d481d5fb1a
2 changed files with 38 additions and 6 deletions
+10 -6
View File
@@ -44,9 +44,12 @@ def _load() -> dict[str, Any]:
logger.warning("Corrupted pairing store, resetting")
return {"approved": {}, "pending": {}}
# Convert approved lists to sets for O(1) lookup
# Convert approved lists to sets for O(1) lookup. Sender IDs are normalized
# to str so lookups match is_approved()/revoke(), which coerce with str():
# IDs may be numeric (e.g. Telegram/QQ) in code or in a hand-edited
# pairing.json, and an int entry would never match the str() lookup.
for channel, users in data.get("approved", {}).items():
data["approved"][channel] = set(users)
data["approved"][channel] = {str(u) for u in users}
return data
@@ -87,7 +90,7 @@ def generate_code(
data.setdefault("pending", {})[code] = {
"channel": channel,
"sender_id": sender_id,
"sender_id": str(sender_id),
"created_at": time.time(),
"expires_at": time.time() + ttl,
}
@@ -162,12 +165,13 @@ def revoke(channel: str, sender_id: str) -> bool:
data = _load()
approved: dict[str, set[str]] = data.get("approved", {})
users = approved.get(channel, set())
if sender_id in users:
users.discard(sender_id)
sid = str(sender_id)
if sid in users:
users.discard(sid)
if not users:
del approved[channel]
_save(data)
logger.info("Revoked {} from {}", sender_id, channel)
logger.info("Revoked {} from {}", sid, channel)
return True
return False
+28
View File
@@ -174,6 +174,34 @@ class TestHandlePairingCommand:
assert "Pending pairing requests:" in reply
class TestNonStringSenderId:
"""Sender IDs may be numeric (e.g. Telegram/QQ). The store normalizes them
to str so writes/reads/removals stay consistent with is_approved()."""
def test_numeric_sender_id_round_trip(self) -> None:
code = store.generate_code("telegram", 12345)
assert store.approve_code(code) == ("telegram", "12345")
# Approved regardless of whether the caller passes int or str.
assert store.is_approved("telegram", 12345) is True
assert store.is_approved("telegram", "12345") is True
assert store.get_approved("telegram") == ["12345"]
# Revoke also works with a numeric id.
assert store.revoke("telegram", 12345) is True
assert store.is_approved("telegram", "12345") is False
def test_numeric_id_in_hand_edited_store(self) -> None:
# Operators may edit pairing.json directly; a numeric entry must still
# match the str() lookup that is_approved() performs.
store._store_path().write_text(
'{"approved": {"telegram": [12345]}, "pending": {}}',
encoding="utf-8",
)
assert store.is_approved("telegram", "12345") is True
assert store.is_approved("telegram", 12345) is True
assert store.revoke("telegram", 12345) is True
assert store.is_approved("telegram", "12345") is False
class TestStoreDurability:
def test_corruption_recovery(self, tmp_path, monkeypatch) -> None:
path = tmp_path / "pairing.json"