138 lines
4.7 KiB
Python
138 lines
4.7 KiB
Python
"""Regression tests for Buzz forum-kind dispatch (#90309).
|
|
|
|
The inbound path hardcoded Nostr kind 9 (chat) at both the WebSocket
|
|
subscription filter and the dispatch gate, so forum channels (kind 45001
|
|
thread roots and 45003 comment replies) were silently never dispatched.
|
|
The DM-reclassification check deliberately stays kind-9-only: widening it
|
|
would let a p-tagged forum post be reclassified as a DM and bypass
|
|
mention gating.
|
|
"""
|
|
|
|
import json
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from gateway.config import PlatformConfig
|
|
from plugins.platforms.buzz.adapter import (
|
|
_CHAT_KIND,
|
|
_DISPATCH_KINDS,
|
|
BuzzAdapter,
|
|
)
|
|
|
|
CHANNEL = "7c83e8f7bb1d4db2bb4074d5c14f2a7f6a9e1c21d3b5a90f8e7d6c5b4a392801"
|
|
SELF_PUBKEY = "aa" * 32
|
|
OTHER_PUBKEY = "bb" * 32
|
|
|
|
|
|
def _dummy_nsec() -> str:
|
|
# Test-only placeholder, assembled to avoid a credential-shaped literal.
|
|
return "-".join(("nsec", "1", "test"))
|
|
|
|
|
|
def _event(event_id, content="hey @Chip", kind=9, p_tag_self=False):
|
|
tags = [["h", CHANNEL]]
|
|
if p_tag_self:
|
|
tags.append(["p", SELF_PUBKEY])
|
|
return {
|
|
"id": event_id,
|
|
"pubkey": OTHER_PUBKEY,
|
|
"content": content,
|
|
"created_at": 100,
|
|
"kind": kind,
|
|
"tags": tags,
|
|
}
|
|
|
|
|
|
def _make_group_adapter():
|
|
adapter = BuzzAdapter(
|
|
PlatformConfig(enabled=True, extra={"relay_url": "https://test.relay"})
|
|
)
|
|
adapter._self_pubkey = SELF_PUBKEY
|
|
adapter._self_npub = "npub1" + "a" * 50
|
|
adapter._display_name = "Chip"
|
|
adapter._private_key = _dummy_nsec()
|
|
adapter._dispatched = []
|
|
|
|
async def capture(**kwargs):
|
|
adapter._dispatched.append(kwargs)
|
|
|
|
adapter._dispatch_message = capture
|
|
adapter._message_handler = AsyncMock()
|
|
adapter._channel_state[CHANNEL] = {"chat_type": "group", "last_ts": 0, "seen": {}}
|
|
return adapter
|
|
|
|
|
|
class _ScriptedCli:
|
|
def __init__(self, events):
|
|
import copy
|
|
|
|
self._events = copy.deepcopy(events)
|
|
|
|
async def __call__(self, args, *, input_text=None):
|
|
return 0, json.dumps(self._events), ""
|
|
|
|
|
|
class TestForumKindDispatch:
|
|
@pytest.mark.asyncio
|
|
async def test_forum_post_root_dispatched(self):
|
|
adapter = _make_group_adapter()
|
|
adapter._run_cli = _ScriptedCli([_event("f1", kind=45001)])
|
|
await adapter._poll_channel(CHANNEL)
|
|
assert [d["message_id"] for d in adapter._dispatched] == ["f1"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_forum_comment_reply_dispatched(self):
|
|
adapter = _make_group_adapter()
|
|
adapter._run_cli = _ScriptedCli([_event("f2", kind=45003)])
|
|
await adapter._poll_channel(CHANNEL)
|
|
assert [d["message_id"] for d in adapter._dispatched] == ["f2"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_undocumented_stream_kind_still_ignored(self):
|
|
"""Stream kinds stay out of scope until their dispatch semantics are
|
|
confirmed — the dispatch set must not widen beyond chat + forum."""
|
|
adapter = _make_group_adapter()
|
|
adapter._run_cli = _ScriptedCli(
|
|
[_event("s1", kind=46010), _event("s2", kind=40007)]
|
|
)
|
|
await adapter._poll_channel(CHANNEL)
|
|
assert adapter._dispatched == []
|
|
|
|
|
|
class TestSubscriptionFilter:
|
|
@pytest.mark.asyncio
|
|
async def test_websocket_subscription_includes_forum_kinds(self):
|
|
"""The REQ filter must subscribe to every dispatchable kind, or the
|
|
live path never even receives forum events to drop."""
|
|
adapter = _make_group_adapter()
|
|
sent = []
|
|
|
|
class _FakeWS:
|
|
async def send(self, payload):
|
|
sent.append(json.loads(payload))
|
|
|
|
adapter._channel_state[CHANNEL]["last_ts"] = 100
|
|
await adapter._send_channel_subscription(_FakeWS(), "sub-1", CHANNEL)
|
|
assert sent, "subscription request must be sent"
|
|
assert sent[0][2]["kinds"] == sorted(_DISPATCH_KINDS)
|
|
assert 45001 in sent[0][2]["kinds"] and 45003 in sent[0][2]["kinds"]
|
|
|
|
|
|
class TestDmReclassificationStaysChatOnly:
|
|
def test_ptagged_forum_post_is_not_a_dm(self):
|
|
"""Security pin: a p-tagged kind-45001 post must NOT be reclassified
|
|
as a DM — that would bypass mention gating for forum content
|
|
(#90309 caveat). The DM check keeps the kind-9-only comparison."""
|
|
adapter = _make_group_adapter()
|
|
adapter._may_reclassify_as_dm = lambda channel_id: True
|
|
assert adapter._is_direct_message_event(
|
|
CHANNEL, _event("f3", kind=45001, p_tag_self=True)
|
|
) is False
|
|
|
|
def test_ptagged_chat_message_can_still_be_a_dm(self):
|
|
adapter = _make_group_adapter()
|
|
adapter._may_reclassify_as_dm = lambda channel_id: True
|
|
event = _event("d1", kind=_CHAT_KIND, p_tag_self=True, content="no mention")
|
|
assert adapter._is_direct_message_event(CHANNEL, event) is True
|