component pubsub: PEP implementation:

New implementations of XEP-0355 and XEP-0356 are integrated to handle PEP. The
implementation is not complete yet, as auto-subscribe and presence permissions are not
implemented yet.

rel 476
This commit is contained in:
Goffi 2026-04-14 23:16:18 +02:00
commit 81bd3dcf01
2 changed files with 244 additions and 153 deletions

View file

@ -20,13 +20,13 @@ from dataclasses import dataclass, field, fields
from typing import Literal, cast
import shortuuid
from twisted.internet import defer
from twisted.words.protocols.jabber import error, jid
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from twisted.words.xish import domish
from tx_xmpp import disco, itx_xmpp
from tx_xmpp import pubsub, rsm
from tx_xmpp.pubsub import NS_PUBSUB_EVENT
from zope.interface import implementer
from libervia.backend import G
@ -44,19 +44,20 @@ from libervia.backend.memory.sqla_mapping import (
PubsubSub,
SubscriptionState,
)
from libervia.backend.plugins.plugin_xep_0167 import NS_AV_CONFERENCES
from libervia.backend.plugins.plugin_xep_0060 import NodeMetadata, XEP_0060
from libervia.backend.plugins.plugin_xep_0167 import NS_AV_CONFERENCES
from libervia.backend.plugins.plugin_xep_0355 import XEP_0355, DelegationData
from libervia.backend.plugins.plugin_xep_0356 import XEP_0356
from libervia.backend.plugins.plugin_xep_0496 import LINK_VAR, PARENT_VAR, XEP_0496
from libervia.backend.plugins.plugin_xep_0499 import (
DiscoPubsubItem,
DiscoPubsubNode,
ExtDiscoOptions,
ExtDiscoResult,
DiscoPubsubNode,
XEP_0499,
)
from libervia.backend.tools.config import ComponentSettings
from libervia.backend.tools.xml_tools import element_copy
from tx_xmpp.pubsub import NS_PUBSUB_EVENT
log = getLogger(__name__)
@ -69,7 +70,7 @@ PLUGIN_INFO = {
C.PI_MODES: [C.PLUG_MODE_COMPONENT],
C.PI_TYPE: C.PLUG_TYPE_ENTRY_POINT,
C.PI_PROTOCOLS: [],
C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0496", "XEP-0499"],
C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0355", "XEP-0356", "XEP-0496", "XEP-0499"],
C.PI_RECOMMENDATIONS: [],
C.PI_MAIN: "PubsubComponent",
C.PI_HANDLER: C.BOOL_TRUE,
@ -187,6 +188,8 @@ class PubsubResource(pubsub.PubSubResource):
def __init__(self, service: "PubsubService") -> None:
super().__init__()
self.service = service
self.xep_0355 = self.service.component.xep_0355
self.xep_0356 = cast(XEP_0356, G.host.plugins["XEP-0356"])
self.xep_0496 = cast(XEP_0496, G.host.plugins["XEP-0496"])
@property
@ -260,7 +263,7 @@ class PubsubResource(pubsub.PubSubResource):
# We only return results for requests without a specific node
return []
nodes = await G.storage.get_pubsub_nodes(self.client, self.client.jid)
nodes = await G.storage.get_pubsub_nodes(self.client, service)
return [cast(str, node.name) for node in nodes]
def getConfigurationOptions(self) -> dict[str, dict]:
@ -290,13 +293,14 @@ class PubsubResource(pubsub.PubSubResource):
requestor_jid = request.sender.userhostJID()
assert request.nodeIdentifier is not None
service_jid = request.recipient if request.recipient else client.jid
if request.items is None or len(request.items) == 0:
raise error.StanzaError("bad-request", text="no item to publish")
node = await G.storage.get_pubsub_node(
client,
client.jid,
service_jid,
request.nodeIdentifier,
with_affiliations=True,
with_parent=True,
@ -307,7 +311,7 @@ class PubsubResource(pubsub.PubSubResource):
raise error.StanzaError("item-not-found", text="Node does not exist")
has_publish_rights = await self.xep_0496.has_publish_rights(
self.client, self.client.jid, node, requestor_jid
self.client, service_jid, node, requestor_jid
)
if not has_publish_rights:
raise error.StanzaError("forbidden", text="Insufficient privileges")
@ -361,6 +365,7 @@ class PubsubResource(pubsub.PubSubResource):
assert request.nodeIdentifier is not None
assert request.subscriber is not None
subscriber_jid = request.subscriber.userhostJID()
service_jid = request.recipient if request.recipient else client.jid
if subscriber_jid != requestor_jid:
raise error.StanzaError(
@ -368,14 +373,14 @@ class PubsubResource(pubsub.PubSubResource):
)
node = await G.storage.get_pubsub_node(
client, client.jid, request.nodeIdentifier, with_affiliations=True
client, service_jid, request.nodeIdentifier, with_affiliations=True
)
if node is None:
raise error.StanzaError("item-not-found", text="Node does not exist")
has_access = await self.xep_0496.has_access(
self.client, self.client.jid, node, subscriber_jid
self.client, service_jid, node, subscriber_jid
)
if not has_access:
raise error.StanzaError(
@ -460,6 +465,7 @@ class PubsubResource(pubsub.PubSubResource):
assert request.nodeIdentifier is not None
assert request.subscriber is not None
subscriber_jid = request.subscriber.userhostJID()
service_jid = request.recipient if request.recipient else client.jid
# Check that the requestor matches the subscriber
if subscriber_jid != requestor_jid:
@ -467,7 +473,9 @@ class PubsubResource(pubsub.PubSubResource):
"forbidden", text="Cannot unsubscribe on behalf of another entity"
)
node = await G.storage.get_pubsub_node(client, client.jid, request.nodeIdentifier)
node = await G.storage.get_pubsub_node(
client, service_jid, request.nodeIdentifier
)
if node is None:
raise error.StanzaError("item-not-found", text="Node does not exist")
@ -495,10 +503,11 @@ class PubsubResource(pubsub.PubSubResource):
client = self.client
assert request.sender is not None
requestor_jid = request.sender.userhostJID()
service_jid = request.recipient if request.recipient else client.jid
subscriptions_data = await G.storage.get_pubsub_subscriptions(
client,
service=client.jid,
service=service_jid,
entity=requestor_jid,
node_name=request.nodeIdentifier,
)
@ -537,10 +546,11 @@ class PubsubResource(pubsub.PubSubResource):
client = self.client
assert request.sender is not None
requestor_jid = request.sender.userhostJID()
service_jid = request.recipient if request.recipient else client.jid
affiliations = await G.storage.get_pubsub_affiliations(
client,
client.jid,
service_jid,
entity=requestor_jid,
node_name=request.nodeIdentifier,
)
@ -561,6 +571,19 @@ class PubsubResource(pubsub.PubSubResource):
client = self.client
assert request.sender is not None
requestor_jid = request.sender.userhostJID()
service_jid = request.recipient if request.recipient else client.jid
is_pep = self.xep_0355.is_pep(client, service_jid)
if is_pep and requestor_jid != service_jid.userhostJID():
log.warning(
f"{requestor_jid} tried to create a PEP node on"
f" {service_jid.userhostJID()}'s service, hack attempt?"
)
raise error.StanzaError(
"forbidden",
text="Only the owner of the PEP service can create PEP nodes.",
)
node_id = request.nodeIdentifier or f"generic/{shortuuid.uuid()}"
@ -612,12 +635,13 @@ class PubsubResource(pubsub.PubSubResource):
node = await G.storage.set_pubsub_node(
client=client,
service=client.jid,
service=service_jid,
name=node_id,
access_model=config.access_model,
publish_model=config.publish_model,
affiliations=[owner_affiliation],
max_items=db_max_items,
pep=is_pep,
)
except exceptions.ConflictError:
raise error.StanzaError("conflict", text=f"node {node_id!r} already exists.")
@ -626,7 +650,7 @@ class PubsubResource(pubsub.PubSubResource):
try:
parent_node, linked_node = await self.xep_0496.check_relationships(
self.client,
self.client.jid,
service_jid,
node,
requestor_jid,
config.parent,
@ -702,10 +726,11 @@ class PubsubResource(pubsub.PubSubResource):
requestor_jid = request.sender.userhostJID()
assert request.nodeIdentifier is not None
service_jid = request.recipient if request.recipient else client.jid
node = await G.storage.get_pubsub_node(
client,
client.jid,
service_jid,
request.nodeIdentifier,
with_affiliations=True,
with_parent=True,
@ -772,10 +797,11 @@ class PubsubResource(pubsub.PubSubResource):
requestor_jid = request.sender.userhostJID()
assert request.nodeIdentifier is not None
service_jid = request.recipient if request.recipient else client.jid
node = await G.storage.get_pubsub_node(
client,
client.jid,
service_jid,
request.nodeIdentifier,
with_affiliations=True,
with_parent=True,
@ -786,7 +812,7 @@ class PubsubResource(pubsub.PubSubResource):
raise error.StanzaError("item-not-found", text="Node does not exist")
has_access = await self.xep_0496.has_access(
self.client, self.client.jid, node, requestor_jid
self.client, service_jid, node, requestor_jid
)
if not has_access:
raise error.StanzaError(
@ -911,7 +937,7 @@ class PubsubResource(pubsub.PubSubResource):
new_parent_node, new_linked_node = (
await self.xep_0496.check_relationships(
self.client,
self.client.jid,
service_jid,
node,
requestor_jid,
new_parent,
@ -975,10 +1001,11 @@ class PubsubResource(pubsub.PubSubResource):
requestor_jid = request.sender.userhostJID()
assert request.nodeIdentifier is not None
service_jid = request.recipient if request.recipient else client.jid
node = await G.storage.get_pubsub_node(
client,
client.jid,
service_jid,
request.nodeIdentifier,
with_affiliations=True,
with_parent=True,
@ -988,7 +1015,7 @@ class PubsubResource(pubsub.PubSubResource):
raise error.StanzaError("item-not-found", text="Node does not exist")
has_access = await self.xep_0496.has_access(
self.client, self.client.jid, node, requestor_jid
self.client, service_jid, node, requestor_jid
)
if not has_access:
raise error.StanzaError("forbidden", text="Insufficient privileges")
@ -1027,13 +1054,14 @@ class PubsubResource(pubsub.PubSubResource):
requestor_jid = request.sender.userhostJID()
assert request.nodeIdentifier is not None
service_jid = request.recipient if request.recipient else client.jid
if not request.itemIdentifiers:
raise error.StanzaError("bad-request", text="No item identifiers provided")
node = await G.storage.get_pubsub_node(
client,
client.jid,
service_jid,
request.nodeIdentifier,
with_affiliations=True,
with_parent=True,
@ -1102,10 +1130,11 @@ class PubsubResource(pubsub.PubSubResource):
requestor_jid = request.sender.userhostJID()
assert request.nodeIdentifier is not None
service_jid = request.recipient if request.recipient else client.jid
node = await G.storage.get_pubsub_node(
client,
client.jid,
service_jid,
request.nodeIdentifier,
with_affiliations=True,
with_parent=True,
@ -1146,10 +1175,11 @@ class PubsubResource(pubsub.PubSubResource):
requestor_jid = request.sender.userhostJID()
assert request.nodeIdentifier is not None
service_jid = request.recipient if request.recipient else client.jid
node = await G.storage.get_pubsub_node(
client,
client.jid,
service_jid,
request.nodeIdentifier,
with_affiliations=True,
with_parent=True,
@ -1160,7 +1190,7 @@ class PubsubResource(pubsub.PubSubResource):
raise error.StanzaError("item-not-found", text="Node does not exist")
has_access = await self.xep_0496.has_access(
self.client, self.client.jid, node, requestor_jid
self.client, service_jid, node, requestor_jid
)
if not has_access:
raise error.StanzaError(
@ -1203,7 +1233,7 @@ class PubsubResource(pubsub.PubSubResource):
if node_name not in nodes_map:
fetched_node = await G.storage.get_pubsub_node(
client,
client.jid,
service_jid,
node_name,
with_affiliations=True,
with_parent=True,
@ -1219,7 +1249,7 @@ class PubsubResource(pubsub.PubSubResource):
if nodes_to_delete:
await G.storage.delete_pubsub_node(
profiles=None, services=[client.jid], names=nodes_to_delete
profiles=None, services=[service_jid], names=nodes_to_delete
)
def affiliationsGet(
@ -1240,16 +1270,17 @@ class PubsubResource(pubsub.PubSubResource):
requestor_jid = request.sender.userhostJID()
assert request.nodeIdentifier is not None
service_jid = request.recipient if request.recipient else client.jid
node = await G.storage.get_pubsub_node(
client, client.jid, request.nodeIdentifier, with_affiliations=True
client, service_jid, request.nodeIdentifier, with_affiliations=True
)
if node is None:
raise error.StanzaError("item-not-found", text="Node does not exist")
has_access = await self.xep_0496.has_access(
self.client, self.client.jid, node, requestor_jid
self.client, service_jid, node, requestor_jid
)
if not has_access:
raise error.StanzaError(
@ -1291,16 +1322,17 @@ class PubsubResource(pubsub.PubSubResource):
requestor_jid = request.sender.userhostJID()
assert request.nodeIdentifier is not None
service_jid = request.recipient if request.recipient else client.jid
node = await G.storage.get_pubsub_node(
client, client.jid, request.nodeIdentifier, with_affiliations=True
client, service_jid, request.nodeIdentifier, with_affiliations=True
)
if node is None:
raise error.StanzaError("item-not-found", text="Node does not exist")
has_access = await self.xep_0496.has_access(
self.client, self.client.jid, node, requestor_jid
self.client, service_jid, node, requestor_jid
)
if not has_access:
raise error.StanzaError(
@ -1348,7 +1380,7 @@ class PubsubResource(pubsub.PubSubResource):
if affiliations_to_set:
await G.storage.set_pubsub_affiliations(
client, client.jid, request.nodeIdentifier, affiliations_to_set
client, service_jid, request.nodeIdentifier, affiliations_to_set
)
def subscriptionsGet(
@ -1371,10 +1403,11 @@ class PubsubResource(pubsub.PubSubResource):
requestor_jid = request.sender.userhostJID()
assert request.nodeIdentifier is not None
service_jid = request.recipient if request.recipient else client.jid
node = await G.storage.get_pubsub_node(
client,
client.jid,
service_jid,
request.nodeIdentifier,
with_subscriptions=True,
with_affiliations=True,
@ -1433,9 +1466,10 @@ class PubsubResource(pubsub.PubSubResource):
requestor_jid = request.sender.userhostJID()
assert request.nodeIdentifier is not None
service_jid = request.recipient if request.recipient else client.jid
node = await G.storage.get_pubsub_node(
client, client.jid, request.nodeIdentifier, with_affiliations=True
client, service_jid, request.nodeIdentifier, with_affiliations=True
)
if node is None:
@ -1550,6 +1584,7 @@ class PubsubResource(pubsub.PubSubResource):
async def _send_notification(
self,
node: PubsubNode,
event_type: NotificationEventType,
node_id: str,
subscriber: jid.JID,
@ -1557,13 +1592,14 @@ class PubsubResource(pubsub.PubSubResource):
) -> None:
"""Send a single notification to a subscriber.
@param node: The pubsub node (to check if it's a PEP node).
@param event_type: The type of event.
@param node_id: The node identifier.
@param subscriber: The subscriber JID.
@param item_elts: The items to include in the notification.
"""
message_elt = domish.Element((None, "message"))
message_elt["from"] = self.client.jid.full()
message_elt["from"] = node.service.full()
message_elt["to"] = subscriber.full()
message_elt["id"] = f"{shortuuid.uuid()}"
@ -1594,7 +1630,10 @@ class PubsubResource(pubsub.PubSubResource):
f"Unknown notification event type: {event_type!r}"
)
await self.client.a_send(message_elt)
if node.pep:
await self.xep_0356.send_privileged_message(self.client, message_elt)
else:
await self.client.a_send(message_elt)
async def _send_notifications(
self,
@ -1616,7 +1655,9 @@ class PubsubResource(pubsub.PubSubResource):
deferreds = [
defer.ensureDeferred(
self._send_notification(event_type, node.name, recipient_jid, item_elts)
self._send_notification(
node, event_type, node.name, recipient_jid, item_elts
)
)
for recipient_jid, __ in recipients.items()
]
@ -1631,8 +1672,8 @@ class PubsubResource(pubsub.PubSubResource):
class PubsubService(rsm.PubSubService):
def __init__(self, component: "PubsubComponent"):
super().__init__(resource=PubsubResource(self))
self.component = component
super().__init__(resource=PubsubResource(self))
self.discoidentity = {
"category": "pubsub",
"type": "service",
@ -1654,11 +1695,19 @@ class PubsubService(rsm.PubSubService):
assert query_elt is not None
node_name = query_elt.getAttribute("node")
requestor_jid = client.jid
try:
service_jid = jid.JID(iq_elt["to"])
except (KeyError, RuntimeError):
service_jid = client.jid
try:
requestor_jid = jid.JID(iq_elt["from"])
except (KeyError, RuntimeError):
requestor_jid = client.jid
nodes = await G.storage.get_pubsub_nodes(
client,
service=client.jid,
service=service_jid,
node_name=node_name,
linked_node=ext_disco_options.linked_nodes,
depth=ext_disco_options.depth,
@ -1672,24 +1721,24 @@ class PubsubService(rsm.PubSubService):
for node in nodes:
if ext_disco_options.depth == 0:
disco_node = DiscoPubsubNode.from_sqlalchemy(node, client.jid)
disco_node = DiscoPubsubNode.from_sqlalchemy(node, service_jid)
if "items" in ext_disco_options.type:
try:
disco_node.items = [
DiscoPubsubItem.from_sqlalchemy(item, node.name, client.jid)
DiscoPubsubItem.from_sqlalchemy(item, node.name, service_jid)
for item in node.items
]
except Exception:
pass
else:
disco_node = DiscoPubsubNode.from_sqlalchemy_full_hierarchy(
node, client.jid
node, service_jid
)
filtered_node = (
await self.component.ext_disco.filter_node_hierarchy_by_access(
client,
client.jid,
service_jid,
disco_node,
requestor_jid,
depth=ext_disco_options.depth,
@ -1714,9 +1763,10 @@ class PubsubComponent:
verbose = 0
def __init__(self, host):
self._clientt: SatXMPPComponent | None = None
self.pubsub_service = PubsubService(self)
self._client: SatXMPPComponent | None = None
self.ext_disco = cast(XEP_0499, host.plugins.get("XEP-0499"))
self.xep_0355 = cast(XEP_0355, G.host.plugins["XEP-0355"])
self.pubsub_service = PubsubService(self)
@property
def client(self) -> SatXMPPComponent:
@ -1729,6 +1779,18 @@ class PubsubComponent:
def profile_connecting(self, client: SatXMPPEntity) -> None:
self._client = cast(SatXMPPComponent, client)
self.ext_disco.register_handler(self.pubsub_service.on_extended_disco)
delegation_data = client.get_plugin_data("XEP_0355", DelegationData)
delegation_data.delegation_received.addCallback(self._on_delegation_received)
def _on_delegation_received(self, __) -> None:
"""Check that pubsub is delegated.
NS_PUBSUB is needed to manage PEP nodes.
"""
if pubsub.NS_PUBSUB not in self.xep_0355.delegated_namespaces:
log.warning(
"Didn't get pubsub delegation from server, can't act as a PEP service"
)
@implementer(itx_xmpp.IDisco)

View file

@ -42,19 +42,34 @@ from libervia.backend.plugins.plugin_xep_0496 import XEP_0496
@fixture(autouse=True)
def setup_xep_0496(host, client):
"""Set up XEP-0496 plugin instance for pubsub tests.
def prepare_for_tests(host, client):
"""Set up important data for testing the pubsub component.
This autouse fixture creates a real XEP_0496 instance so that the actual
permission-checking logic is tested rather than mocked behavior.
It also links the client to the host so PubsubResource can access the plugin.
This creates a real XEP_0496 instance so that the actual permission-checking logic is
tested rather than mocked behavior.
It also links the client to the host so PubsubResource can access the plugin, and mark
it as a component to reflect real workflow.
"""
xep_0496 = XEP_0496(host)
host.plugins["XEP-0496"] = xep_0496
client.is_component = True
client.host = host
return xep_0496
@fixture
def pubsub_resource(client):
"""Create a PubsubResource with proper mocking.
This fixture creates a PubsubResource with a mock service that has
xep_0355.is_pep configured to return False by default (since
pubsub.test.example is not a PEP service).
"""
mock_service = MagicMock(client=client)
mock_service.component.xep_0355.is_pep.return_value = False
return PubsubResource(mock_service)
def make_pubsub_request(
sender: jid.JID,
nodeIdentifier: str | None = None,
@ -68,6 +83,7 @@ def make_pubsub_request(
maxItems: int | None = None,
nodeType: str | None = None,
notify: bool = False,
recipient: jid.JID | None = None,
) -> pubsub.PubSubRequest:
"""Helper to create a PubSubRequest for testing."""
request = pubsub.PubSubRequest()
@ -83,6 +99,7 @@ def make_pubsub_request(
request.maxItems = maxItems
request.nodeType = nodeType
request.notify = notify
request.recipient = recipient
return request
@ -132,17 +149,17 @@ class TestPubsubResourceCreate:
"""Tests for node creation."""
@ed
async def test_creates_node_with_owner_affiliation(self, host, client):
async def test_creates_node_with_owner_affiliation(self, host, pubsub_resource):
"""creating a node sets the creator as owner"""
resource = PubsubResource(MagicMock(client=client))
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
recipient=pubsub_resource.service.client.pubsub_service,
)
host.memory.storage.set_pubsub_node = AsyncMock()
await resource._create(request)
await pubsub_resource._create(request)
# Verify that the creator is set as owner
call_args = host.memory.storage.set_pubsub_node.call_args
@ -153,14 +170,16 @@ class TestPubsubResourceCreate:
assert affiliations[0].affiliation == Affiliation.owner
@ed
async def test_generates_node_id_if_not_provided(self, host, client):
async def test_generates_node_id_if_not_provided(self, host, pubsub_resource):
"""creating a node without ID generates one"""
resource = PubsubResource(MagicMock(client=client))
request = make_pubsub_request(sender=jid.JID("user@test.example"))
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
recipient=pubsub_resource.service.client.pubsub_service,
)
host.memory.storage.set_pubsub_node = AsyncMock()
await resource._create(request)
await pubsub_resource._create(request)
# Verify a node ID was generated and used
call_args = host.memory.storage.set_pubsub_node.call_args
@ -172,14 +191,14 @@ class TestPubsubResourceCreate:
assert node_id.startswith("generic/")
@ed
async def test_raises_conflict_if_node_exists(self, host, client):
async def test_raises_conflict_if_node_exists(self, host, pubsub_resource):
"""creating an existing node raises conflict"""
from libervia.backend.core.exceptions import ConflictError
resource = PubsubResource(MagicMock(client=client))
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="existing_node",
recipient=pubsub_resource.service.client.pubsub_service,
)
# The ConflictError is generated by storage, so we actually check that the
@ -187,16 +206,16 @@ class TestPubsubResourceCreate:
host.memory.storage.set_pubsub_node = AsyncMock(side_effect=ConflictError())
with raises(StanzaError, match="conflict"):
await resource._create(request)
await pubsub_resource._create(request)
class TestPubsubResourcePublish:
"""Tests for item publishing."""
@ed
async def test_rejects_empty_items(self, host, client):
async def test_rejects_empty_items(self, host, client, pubsub_resource):
"""publishing empty items raises bad-request"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -207,9 +226,9 @@ class TestPubsubResourcePublish:
await resource._publish(request)
@ed
async def test_rejects_nonexistent_node(self, host, client):
async def test_rejects_nonexistent_node(self, host, client, pubsub_resource):
"""publishing to nonexistent node raises item_not_found"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="nonexistent_node",
@ -222,9 +241,11 @@ class TestPubsubResourcePublish:
await resource._publish(request)
@ed
async def test_publishers_model_requires_publisher_affiliation(self, host, client):
async def test_publishers_model_requires_publisher_affiliation(
self, host, client, pubsub_resource
):
"""publishers model requires publisher or owner affiliation"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -244,9 +265,9 @@ class TestPubsubResourcePublish:
await resource._publish(request)
@ed
async def test_publishers_model_allows_owner(self, host, client):
async def test_publishers_model_allows_owner(self, host, client, pubsub_resource):
"""publishers model allows owners to publish"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("owner@test.example"),
nodeIdentifier="test_node",
@ -268,9 +289,9 @@ class TestPubsubResourcePublish:
host.memory.storage.cache_pubsub_items.assert_called_once()
@ed
async def test_publishers_model_allows_publisher(self, host, client):
async def test_publishers_model_allows_publisher(self, host, client, pubsub_resource):
"""publishers model allows publishers to publish"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("publisher@test.example"),
nodeIdentifier="test_node",
@ -292,9 +313,11 @@ class TestPubsubResourcePublish:
host.memory.storage.cache_pubsub_items.assert_called_once()
@ed
async def test_subscribers_model_requires_subscription(self, host, client):
async def test_subscribers_model_requires_subscription(
self, host, client, pubsub_resource
):
"""subscribers model requires subscription to publish"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -314,9 +337,11 @@ class TestPubsubResourcePublish:
await resource._publish(request)
@ed
async def test_subscribers_model_allows_subscribed_member(self, host, client):
async def test_subscribers_model_allows_subscribed_member(
self, host, client, pubsub_resource
):
"""subscribers model allows subscribed members to publish"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -339,9 +364,9 @@ class TestPubsubResourcePublish:
host.memory.storage.cache_pubsub_items.assert_called_once()
@ed
async def test_generates_item_id_if_missing(self, host, client):
async def test_generates_item_id_if_missing(self, host, client, pubsub_resource):
"""publishing an item without ID generates one"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
item = make_item_element("") # Empty ID
request = make_pubsub_request(
sender=jid.JID("owner@test.example"),
@ -370,9 +395,9 @@ class TestPubsubResourcePublish:
assert len(items_data[0].getAttribute("id")) > 0
@ed
async def test_sets_publisher_attribute(self, host, client):
async def test_sets_publisher_attribute(self, host, client, pubsub_resource):
"""publishing sets the publisher attribute on items"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
item = make_item_element("item1")
request = make_pubsub_request(
sender=jid.JID("user@test.example/resource"),
@ -405,9 +430,9 @@ class TestPubsubResourceSubscribe:
"""Tests for subscription management."""
@ed
async def test_rejects_subscribing_for_others(self, host, client):
async def test_rejects_subscribing_for_others(self, host, client, pubsub_resource):
"""subscribing for another user raises forbidden"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -418,9 +443,9 @@ class TestPubsubResourceSubscribe:
await resource._subscribe(request)
@ed
async def test_rejects_nonexistent_node(self, host, client):
async def test_rejects_nonexistent_node(self, host, client, pubsub_resource):
"""subscribing to nonexistent node raises item_not_found"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="nonexistent_node",
@ -433,9 +458,9 @@ class TestPubsubResourceSubscribe:
await resource._subscribe(request)
@ed
async def test_rejects_outcast_users(self, host, client):
async def test_rejects_outcast_users(self, host, client, pubsub_resource):
"""outcast users cannot subscribe"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("outcast@test.example"),
nodeIdentifier="test_node",
@ -454,9 +479,9 @@ class TestPubsubResourceSubscribe:
await resource._subscribe(request)
@ed
async def test_open_model_allows_anyone(self, host, client):
async def test_open_model_allows_anyone(self, host, client, pubsub_resource):
"""open access model allows anyone to subscribe"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -481,9 +506,11 @@ class TestPubsubResourceSubscribe:
)
@ed
async def test_whitelist_model_requires_membership(self, host, client):
async def test_whitelist_model_requires_membership(
self, host, client, pubsub_resource
):
"""whitelist access model requires member affiliation"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -501,9 +528,9 @@ class TestPubsubResourceSubscribe:
await resource._subscribe(request)
@ed
async def test_whitelist_model_allows_member(self, host, client):
async def test_whitelist_model_allows_member(self, host, client, pubsub_resource):
"""whitelist access model allows members to subscribe"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("member@test.example"),
nodeIdentifier="test_node",
@ -531,9 +558,9 @@ class TestPubsubResourceSubscribe:
)
@ed
async def test_rejects_duplicate_pending(self, host, client):
async def test_rejects_duplicate_pending(self, host, client, pubsub_resource):
"""subscribing when already pending raises not-authorized"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -558,9 +585,9 @@ class TestPubsubResourceUnsubscribe:
"""Tests for unsubscription."""
@ed
async def test_rejects_unsubscribing_for_others(self, host, client):
async def test_rejects_unsubscribing_for_others(self, host, client, pubsub_resource):
"""unsubscribing for another user raises forbidden"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -571,9 +598,9 @@ class TestPubsubResourceUnsubscribe:
await resource._unsubscribe(request)
@ed
async def test_rejects_nonexistent_node(self, host, client):
async def test_rejects_nonexistent_node(self, host, client, pubsub_resource):
"""unsubscribing from nonexistent node raises item_not_found"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="nonexistent_node",
@ -586,9 +613,9 @@ class TestPubsubResourceUnsubscribe:
await resource._unsubscribe(request)
@ed
async def test_removes_subscription(self, host, client):
async def test_removes_subscription(self, host, client, pubsub_resource):
"""unsubscribing removes the subscription"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -617,9 +644,9 @@ class TestPubsubResourceItems:
"""Tests for item retrieval."""
@ed
async def test_rejects_nonexistent_node(self, host, client):
async def test_rejects_nonexistent_node(self, host, client, pubsub_resource):
"""retrieving items from nonexistent node raises item_not_found"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="nonexistent_node",
@ -631,9 +658,9 @@ class TestPubsubResourceItems:
await resource._items(request)
@ed
async def test_open_model_allows_anyone(self, host, client):
async def test_open_model_allows_anyone(self, host, client, pubsub_resource):
"""open access model allows anyone to retrieve items"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -651,9 +678,11 @@ class TestPubsubResourceItems:
host.memory.storage.get_items.assert_called_once()
@ed
async def test_whitelist_model_rejects_non_members(self, host, client):
async def test_whitelist_model_rejects_non_members(
self, host, client, pubsub_resource
):
"""whitelist access model rejects non-members"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -670,9 +699,9 @@ class TestPubsubResourceItems:
await resource._items(request)
@ed
async def test_whitelist_model_allows_member(self, host, client):
async def test_whitelist_model_allows_member(self, host, client, pubsub_resource):
"""whitelist access model allows members to retrieve items"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("member@test.example"),
nodeIdentifier="test_node",
@ -697,9 +726,9 @@ class TestPubsubResourceRetract:
"""Tests for item retraction."""
@ed
async def test_rejects_empty_identifiers(self, host, client):
async def test_rejects_empty_identifiers(self, host, client, pubsub_resource):
"""retracting without item identifiers raises bad-request"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -710,9 +739,9 @@ class TestPubsubResourceRetract:
await resource._retract(request)
@ed
async def test_rejects_nonexistent_node(self, host, client):
async def test_rejects_nonexistent_node(self, host, client, pubsub_resource):
"""retracting from nonexistent node raises item_not_found"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="nonexistent_node",
@ -725,9 +754,9 @@ class TestPubsubResourceRetract:
await resource._retract(request)
@ed
async def test_owner_can_retract_any_item(self, host, client):
async def test_owner_can_retract_any_item(self, host, client, pubsub_resource):
"""owners can retract any item from their node"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("owner@test.example"),
nodeIdentifier="test_node",
@ -760,9 +789,9 @@ class TestPubsubResourceRetract:
assert call_args.args[1] == ["item1"]
@ed
async def test_publisher_can_retract_any_item(self, host, client):
async def test_publisher_can_retract_any_item(self, host, client, pubsub_resource):
"""publishers can retract any item from their node"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("publisher@test.example"),
nodeIdentifier="test_node",
@ -794,9 +823,9 @@ class TestPubsubResourceRetract:
assert call_args.args[1] == ["item1"]
@ed
async def test_user_can_only_retract_own_items(self, host, client):
async def test_user_can_only_retract_own_items(self, host, client, pubsub_resource):
"""non-owners can only retract their own items"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -824,9 +853,9 @@ class TestPubsubResourceRetract:
await resource._retract(request)
@ed
async def test_user_can_retract_own_items(self, host, client):
async def test_user_can_retract_own_items(self, host, client, pubsub_resource):
"""users can retract their own items"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -863,9 +892,9 @@ class TestPubsubResourcePurge:
"""Tests for node purging."""
@ed
async def test_rejects_nonexistent_node(self, host, client):
async def test_rejects_nonexistent_node(self, host, client, pubsub_resource):
"""purging nonexistent node raises item_not_found"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="nonexistent_node",
@ -877,9 +906,9 @@ class TestPubsubResourcePurge:
await resource._purge(request)
@ed
async def test_only_owners_can_purge(self, host, client):
async def test_only_owners_can_purge(self, host, client, pubsub_resource):
"""only owners can purge a node"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -898,9 +927,9 @@ class TestPubsubResourcePurge:
await resource._purge(request)
@ed
async def test_owner_can_purge(self, host, client):
async def test_owner_can_purge(self, host, client, pubsub_resource):
"""owners can purge their node"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("owner@test.example"),
nodeIdentifier="test_node",
@ -929,9 +958,9 @@ class TestPubsubResourceDelete:
"""Tests for node deletion."""
@ed
async def test_rejects_nonexistent_node(self, host, client):
async def test_rejects_nonexistent_node(self, host, client, pubsub_resource):
"""deleting nonexistent node raises item_not_found"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="nonexistent_node",
@ -943,9 +972,9 @@ class TestPubsubResourceDelete:
await resource._delete(request)
@ed
async def test_only_owners_can_delete(self, host, client):
async def test_only_owners_can_delete(self, host, client, pubsub_resource):
"""only owners can delete a node"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -964,9 +993,9 @@ class TestPubsubResourceDelete:
await resource._delete(request)
@ed
async def test_owner_can_delete(self, host, client):
async def test_owner_can_delete(self, host, client, pubsub_resource):
"""owners can delete their node"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("owner@test.example"),
nodeIdentifier="test_node",
@ -995,9 +1024,9 @@ class TestPubsubResourceConfigure:
"""Tests for node configuration."""
@ed
async def test_only_owners_can_get_config(self, host, client):
async def test_only_owners_can_get_config(self, host, client, pubsub_resource):
"""only owners can get node configuration"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -1016,9 +1045,9 @@ class TestPubsubResourceConfigure:
await resource._configureGet(request)
@ed
async def test_owner_can_get_config(self, host, client):
async def test_owner_can_get_config(self, host, client, pubsub_resource):
"""owners can get node configuration"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("owner@test.example"),
nodeIdentifier="test_node",
@ -1041,9 +1070,9 @@ class TestPubsubResourceConfigure:
assert result["pubsub#publish_model"] == "subscribers"
@ed
async def test_only_owners_can_set_config(self, host, client):
async def test_only_owners_can_set_config(self, host, client, pubsub_resource):
"""only owners can set node configuration"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -1063,9 +1092,9 @@ class TestPubsubResourceConfigure:
await resource._configureSet(request)
@ed
async def test_owner_can_set_config(self, host, client):
async def test_owner_can_set_config(self, host, client, pubsub_resource):
"""owners can set node configuration"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("owner@test.example"),
nodeIdentifier="test_node",
@ -1098,9 +1127,9 @@ class TestPubsubResourceAffiliations:
"""Tests for affiliation management."""
@ed
async def test_only_owners_can_get_affiliations(self, host, client):
async def test_only_owners_can_get_affiliations(self, host, client, pubsub_resource):
"""only owners can retrieve all affiliations"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -1119,9 +1148,9 @@ class TestPubsubResourceAffiliations:
await resource._affiliationsGet(request)
@ed
async def test_only_owners_can_set_affiliations(self, host, client):
async def test_only_owners_can_set_affiliations(self, host, client, pubsub_resource):
"""only owners can set affiliations"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -1141,9 +1170,9 @@ class TestPubsubResourceAffiliations:
await resource._affiliationsSet(request)
@ed
async def test_owner_cannot_demote_self(self, host, client):
async def test_owner_cannot_demote_self(self, host, client, pubsub_resource):
"""owners cannot change their own affiliation to non-owner"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("owner@test.example"),
nodeIdentifier="test_node",
@ -1167,9 +1196,9 @@ class TestPubsubResourceSubscriptionsGetSet:
"""Tests for subscription management by owners."""
@ed
async def test_only_owners_can_get_subscriptions(self, host, client):
async def test_only_owners_can_get_subscriptions(self, host, client, pubsub_resource):
"""only owners can retrieve all subscriptions"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -1188,9 +1217,9 @@ class TestPubsubResourceSubscriptionsGetSet:
await resource._subscriptionsGet(request)
@ed
async def test_only_owners_can_set_subscriptions(self, host, client):
async def test_only_owners_can_set_subscriptions(self, host, client, pubsub_resource):
"""only owners can set subscriptions"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("user@test.example"),
nodeIdentifier="test_node",
@ -1216,9 +1245,9 @@ class TestPubsubResourceSubscriptionsGetSet:
await resource._subscriptionsSet(request)
@ed
async def test_owner_can_set_subscriptions(self, host, client):
async def test_owner_can_set_subscriptions(self, host, client, pubsub_resource):
"""owners can set subscriptions for entities"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("owner@test.example"),
nodeIdentifier="test_node",
@ -1252,9 +1281,9 @@ class TestPubsubResourceSubscriptionsGetSet:
)
@ed
async def test_cannot_subscribe_outcast(self, host, client):
async def test_cannot_subscribe_outcast(self, host, client, pubsub_resource):
"""owners cannot subscribe outcast entities"""
resource = PubsubResource(MagicMock(client=client))
resource = pubsub_resource
request = make_pubsub_request(
sender=jid.JID("owner@test.example"),
nodeIdentifier="test_node",