# HG changeset patch # User Arnaud Joset # Date 1509659459 -3600 # Node ID c8cb4e867897a830da6b2fa0957cd3041aa4c338 # Parent 7430d1f6db229269e5509bd2ca2976509c3f09b7 made proper package + installation diff -r 7430d1f6db22 -r c8cb4e867897 __init__.py diff -r 7430d1f6db22 -r c8cb4e867897 import_test.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/import_test.py Thu Nov 02 22:50:59 2017 +0100 @@ -0,0 +1,9 @@ +import wokkel + +from sat_tmp.wokkel import pubsub as tmp_pubsub, rsm as tmp_rsm, mam as tmp_mam + +wokkel.pubsub = tmp_pubsub + +wokkel.rsm = tmp_rsm + +wokkel.mam = tmp_mam diff -r 7430d1f6db22 -r c8cb4e867897 sat_tmp/__init__.py diff -r 7430d1f6db22 -r c8cb4e867897 sat_tmp/wokkel/__init__.py diff -r 7430d1f6db22 -r c8cb4e867897 sat_tmp/wokkel/mam.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat_tmp/wokkel/mam.py Thu Nov 02 22:50:59 2017 +0100 @@ -0,0 +1,620 @@ +# -*- coding: utf-8 -*- +# -*- test-case-name: wokkel.test.test_mam -*- +# +# SàT Wokkel extension for Message Archive Management (XEP-0313) +# Copyright (C) 2015 Jérôme Poisson (goffi@goffi.org) +# Copyright (C) 2015 Adien Cossa (souliane@mailoo.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +""" +XMPP Message Archive Management protocol. + +This protocol is specified in +U{XEP-0313}. +""" + +from dateutil import tz + +from zope.interface import implements +from zope.interface import Interface + +from twisted.words.protocols.jabber import xmlstream +from twisted.words.xish import domish +from twisted.words.protocols.jabber import jid +from twisted.words.protocols.jabber import error +from twisted.internet import defer +from twisted.python import log + +from wokkel import subprotocols +from wokkel import disco +from wokkel import data_form +from wokkel import delay + +import rsm + +NS_MAM = 'urn:xmpp:mam:1' +NS_FORWARD = 'urn:xmpp:forward:0' + +FIELDS_REQUEST = "/iq[@type='get']/query[@xmlns='%s']" % NS_MAM +ARCHIVE_REQUEST = "/iq[@type='set']/query[@xmlns='%s']" % NS_MAM +PREFS_GET_REQUEST = "/iq[@type='get']/prefs[@xmlns='%s']" % NS_MAM +PREFS_SET_REQUEST = "/iq[@type='set']/prefs[@xmlns='%s']" % NS_MAM + +# TODO: add the tests! + + +class MAMError(error.StanzaError): + """ + MAM error. + """ + def __init__(self, text=None): + error.StanzaError.__init__(self, 'bad-request', text=text) + + +class Unsupported(MAMError): + def __init__(self, feature, text=None): + self.feature = feature + MAMError.__init__(self, 'feature-not-implemented', + 'unsupported', + feature, + text) + + def __str__(self): + message = MAMError.__str__(self) + message += ', feature %r' % self.feature + return message + + +class MAMRequest(object): + """ + A Message Archive Management request. + + @ivar form: Data Form specifing the filters. + @itype form: L{data_form.Form} + + @ivar rsm: RSM request instance. + @itype rsm: L{rsm.RSMRequest} + + @ivar node: pubsub node id if querying a pubsub node, else None. + @itype node: C{unicode} + + @ivar query_id: id to use to track the query + @itype query_id: C{unicode} + """ + # FIXME: should be based on generic.Stanza + + def __init__(self, form=None, rsm_=None, node=None, query_id=None, sender=None, recipient=None): + if form is not None: + assert form.formType == 'submit' + assert form.formNamespace == NS_MAM + self.form = form + self.rsm = rsm_ + self.node = node + self.query_id = query_id + self.sender = sender + self.recipient = recipient + + @classmethod + def fromElement(cls, iq): + """Parse the DOM representation of a MAM request. + + @param iq: element containing a MAM . + @type iq: L{Element} + + @return: MAMRequest instance. + @rtype: L{MAMRequest} + """ + sender = jid.JID(iq.getAttribute('from')) + recipient = jid.JID(iq.getAttribute('to')) + try: + query = iq.elements(NS_MAM, 'query').next() + except StopIteration: + raise MAMError("Can't find MAM in element") + form = data_form.findForm(query, NS_MAM) + try: + rsm_request = rsm.RSMRequest.fromElement(query) + except rsm.RSMNotFoundError: + rsm_request = None + node = query.getAttribute('node') + query_id = query.getAttribute('queryid') + return MAMRequest(form, rsm_request, node, query_id, sender, recipient) + + def toElement(self): + """ + Return the DOM representation of this RSM request. + + @rtype: L{Element} + """ + mam_elt = domish.Element((NS_MAM, 'query')) + if self.node is not None: + mam_elt['node'] = self.node + if self.query_id is not None: + mam_elt['queryid'] = self.query_id + if self.form is not None: + mam_elt.addChild(self.form.toElement()) + if self.rsm is not None: + mam_elt.addChild(self.rsm.toElement()) + + return mam_elt + + def render(self, parent): + """Embed the DOM representation of this MAM request in the given element. + + @param parent: parent IQ element. + @type parent: L{Element} + + @return: MAM request element. + @rtype: L{Element} + """ + assert parent.name == 'iq' + mam_elt = self.toElement() + parent.addChild(mam_elt) + return mam_elt + + +class MAMPrefs(object): + """ + A Message Archive Management request. + + @param default: A value in ('always', 'never', 'roster'). + @type : C{unicode} or C{None} + + @param always (list): A list of JID instances. + @type always: C{list} + + @param never (list): A list of JID instances. + @type never: C{list} + """ + + def __init__(self, default=None, always=None, never=None): + if default is not None: + # default must be defined in response, but can be empty in request (see http://xmpp.org/extensions/xep-0313.html#config) + assert default in ('always', 'never', 'roster') + self.default = default + if always is not None: + assert isinstance(always, list) + else: + always = [] + self.always = always + if never is not None: + assert isinstance(never, list) + else: + never = [] + self.never = never + + @classmethod + def fromElement(cls, prefs_elt): + """Parse the DOM representation of a MAM request. + + @param prefs_elt: MAM request element. + @type prefs_elt: L{Element} + + @return: MAMPrefs instance. + @rtype: L{MAMPrefs} + """ + if prefs_elt.uri != NS_MAM or prefs_elt.name != 'prefs': + raise MAMError('Element provided is not a MAM request') + try: + default = prefs_elt['default'] + except KeyError: + # FIXME: return proper error here + raise MAMError('Element provided is not a valid MAM request') + + prefs = {} + for attr in ('always', 'never'): + prefs[attr] = [] + try: + pref = prefs_elt.elements(NS_MAM, attr).next() + except StopIteration: + # FIXME: return proper error here + raise MAMError('Element provided is not a valid MAM request') + else: + for jid_s in pref.elements(NS_MAM, 'jid'): + prefs[attr].append(jid.JID(jid_s)) + return MAMPrefs(default, **prefs) + + def toElement(self): + """ + Return the DOM representation of this RSM request. + + @rtype: L{Element} + """ + mam_elt = domish.Element((NS_MAM, 'prefs')) + if self.default: + mam_elt['default'] = self.default + for attr in ('always', 'never'): + attr_elt = mam_elt.addElement(attr) + jids = getattr(self, attr) + for jid_ in jids: + attr_elt.addElement('jid', content=jid_.full()) + return mam_elt + + def render(self, parent): + """Embed the DOM representation of this MAM request in the given element. + + @param parent: parent IQ element. + @type parent: L{Element} + + @return: MAM request element. + @rtype: L{Element} + """ + assert parent.name == 'iq' + mam_elt = self.toElement() + parent.addChild(mam_elt) + return mam_elt + + +class MAMClient(subprotocols.XMPPHandler): + """ + MAM client. + + This handler implements the protocol for sending out MAM requests. + """ + + def queryArchive(self, mam_query, service=None, sender=None): + """Query a user, MUC or pubsub archive. + + @param mam_query: query to use + @type form: L{MAMRequest} + + @param service: Entity offering the MAM service (None for user server). + @type service: L{JID} + + @param sender: Optional sender address. + @type sender: L{JID} + + @return: A deferred that fires upon receiving a response. + @rtype: L{Deferred} + """ + iq = xmlstream.IQ(self.xmlstream, 'set') + mam_query.render(iq) + if sender is not None: + iq['from'] = unicode(sender) + return iq.send(to=service.full() if service else None) + + def queryFields(self, service=None, sender=None): + """Ask the server about supported fields. + + @param service: Entity offering the MAM service (None for user archives). + @type service: L{JID} + + @param sender: Optional sender address. + @type sender: L{JID} + + @return: data Form with the fields, or None if not found + @rtype: L{Deferred} + """ + # http://xmpp.org/extensions/xep-0313.html#query-form + iq = xmlstream.IQ(self.xmlstream, 'get') + MAMRequest().render(iq) + if sender is not None: + iq['from'] = unicode(sender) + d = iq.send(to=service.full() if service else None) + d.addCallback(lambda iq_result: iq_result.elements(NS_MAM, 'query').next()) + d.addCallback(data_form.findForm, NS_MAM) + return d + + def queryPrefs(self, service=None, sender=None): + """Retrieve the current user preferences. + + @param service: Entity offering the MAM service (None for user archives). + @type service: L{JID} + + @param sender: Optional sender address. + @type sender: L{JID} + + @return: A deferred that fires upon receiving a response. + @rtype: L{Deferred} + """ + # http://xmpp.org/extensions/xep-0313.html#prefs + iq = xmlstream.IQ(self.xmlstream, 'get') + MAMPrefs().render(iq) + if sender is not None: + iq['from'] = unicode(sender) + return iq.send(to=service.full() if service else None) + + def setPrefs(self, service=None, default='roster', always=None, never=None, sender=None): + """Set new user preferences. + + @param service: Entity offering the MAM service (None for user archives). + @type service: L{JID} + + @param default: A value in ('always', 'never', 'roster'). + @type : C{unicode} + + @param always (list): A list of JID instances. + @type always: C{list} + + @param never (list): A list of JID instances. + @type never: C{list} + + @param sender: Optional sender address. + @type sender: L{JID} + + @return: A deferred that fires upon receiving a response. + @rtype: L{Deferred} + """ + # http://xmpp.org/extensions/xep-0313.html#prefs + assert default is not None + iq = xmlstream.IQ(self.xmlstream, 'set') + MAMPrefs(default, always, never).render(iq) + if sender is not None: + iq['from'] = unicode(sender) + return iq.send(to=service.full() if service else None) + + +class IMAMResource(Interface): + + def onArchiveRequest(self, mam): + """ + + @param mam: The MAM request. + @type mam: L{MAMQueryReques} + + @return: The RSM answer. + @rtype: L{RSMResponse} + """ + + def onPrefsGetRequest(self, requestor): + """ + + @param requestor: JID of the requestor. + @type requestor: L{JID} + + @return: The current settings. + @rtype: L{wokkel.mam.MAMPrefs} + """ + + def onPrefsSetRequest(self, prefs, requestor): + """ + + @param prefs: The new settings to set. + @type prefs: L{wokkel.mam.MAMPrefs} + + @param requestor: JID of the requestor. + @type requestor: L{JID} + + @return: The new current settings. + @rtype: L{wokkel.mam.MAMPrefs} + """ + +class IMAMService(Interface): + """ + Interface for XMPP MAM service. + """ + + def addFilter(self, field): + """ + Add a new filter for querying MAM archive. + + @param field: data form field of the filter + @type field: L{Form} + """ + + +class MAMService(subprotocols.XMPPHandler, subprotocols.IQHandlerMixin): + """ + Protocol implementation for a MAM service. + + This handler waits for XMPP Ping requests and sends a response. + """ + implements(IMAMService, disco.IDisco) + + _request_class = MAMRequest + + iqHandlers = {FIELDS_REQUEST: '_onFieldsRequest', + ARCHIVE_REQUEST: '_onArchiveRequest', + PREFS_GET_REQUEST: '_onPrefsGetRequest', + PREFS_SET_REQUEST: '_onPrefsSetRequest' + } + + _legacyFilters = {'start': {'fieldType': 'text-single', + 'var': 'start', + 'label': 'Starting time', + 'desc': 'Starting time a the result period.', + }, + 'end': {'fieldType': 'text-single', + 'var': 'end', + 'label': 'Ending time', + 'desc': 'Ending time of the result period.', + }, + 'with': {'fieldType': 'jid-single', + 'var': 'with', + 'label': 'Entity', + 'desc': 'Entity against which to match message.', + }, + } + + def __init__(self, resource): + """ + @param resource: instance implementing IMAMResource + @type resource: L{object} + """ + self.resource = resource + self.extra_fields = {} + + def connectionInitialized(self): + """ + Called when the XML stream has been initialized. + + This sets up an observer for incoming ping requests. + """ + self.xmlstream.addObserver(FIELDS_REQUEST, self.handleRequest) + self.xmlstream.addObserver(ARCHIVE_REQUEST, self.handleRequest) + self.xmlstream.addObserver(PREFS_GET_REQUEST, self.handleRequest) + self.xmlstream.addObserver(PREFS_SET_REQUEST, self.handleRequest) + + def addFilter(self, field): + """ + Add a new filter for querying MAM archive. + + @param field: data form field of the filter + @type field: L{Form} + """ + self.extra_fields[field.var] = field + + def _onFieldsRequest(self, iq): + """ + Called when a fields request has been received. + + This immediately replies with a result response. + """ + iq.handled = True + query = domish.Element((NS_MAM, 'query')) + query.addChild(buildForm(extra_fields=self.extra_fields).toElement(), formType='form') + return query + + def _onArchiveRequest(self, iq): + """ + Called when a message archive request has been received. + + This replies with the list of archived message and the result + @return: A tuple with list of message data (id, element, data) and RSM element + @rtype: C{tuple} + """ + iq.handled = True + mam_ = self._request_class.fromElement(iq) + + # remove unsupported filters + unsupported_fields = [] + if mam_.form: + for key, field in mam_.form.fields.iteritems(): + if key not in self._legacyFilters and key not in self.extra_fields: + log.msg('Ignored unsupported MAM filter: %s' % field) + unsupported_fields.append(key) + for key in unsupported_fields: + del mam_.form.fields[key] + + def forwardMessage(id_, elt, date): + msg = domish.Element((None, 'message')) + msg['to'] = iq['from'] + result = msg.addElement((NS_MAM, 'result')) + if mam_.query_id is not None: + result['queryid'] = mam_.query_id + result['id'] = id_ + forward = result.addElement((NS_FORWARD, 'forwarded')) + forward.addChild(delay.Delay(date).toElement()) + forward.addChild(elt) + self.xmlstream.send(msg) + + def cb(result): + msg_data, rsm_elt = result + for data in msg_data: + forwardMessage(*data) + + fin_elt = domish.Element((NS_MAM, 'fin')) + + if rsm_elt is not None: + fin_elt.addChild(rsm_elt) + return fin_elt + + d = defer.maybeDeferred(self.resource.onArchiveRequest, mam_) + d.addCallback(cb) + return d + + def _onPrefsGetRequest(self, iq): + """ + Called when a prefs get request has been received. + + This immediately replies with a result response. + """ + iq.handled = True + requestor = jid.JID(iq['from']) + + def cb(prefs): + return prefs.toElement() + + d = self.resource.onPrefsGetRequest(requestor).addCallback(cb) + return d + + def _onPrefsSetRequest(self, iq): + """ + Called when a prefs get request has been received. + + This immediately replies with a result response. + """ + iq.handled = True + + prefs = MAMPrefs.fromElement(iq.prefs) + requestor = jid.JID(iq['from']) + + def cb(prefs): + return prefs.toElement() + + d = self.resource.onPrefsSetRequest(prefs, requestor).addCallback(cb) + return d + + def getDiscoInfo(self, requestor, target, nodeIdentifier=''): + if nodeIdentifier: + return [] + return [disco.DiscoFeature(NS_MAM)] + + def getDiscoItems(self, requestor, target, nodeIdentifier=''): + return [] + + +def datetime2utc(datetime_obj): + """Convert a datetime to a XEP-0082 compliant UTC datetime. + + @param datetime_obj: Offset-aware timestamp to convert. + @type datetime_obj: L{datetime} + + @return: The datetime converted to UTC. + @rtype: C{unicode} + """ + stampFormat = '%Y-%m-%dT%H:%M:%SZ' + return datetime_obj.astimezone(tz.tzutc()).strftime(stampFormat) + + +def buildForm(start=None, end=None, with_jid=None, extra_fields=None, formType='submit'): + """Prepare a Data Form for MAM. + + @param start: Offset-aware timestamp to filter out older messages. + @type start: L{datetime} + + @param end: Offset-aware timestamp to filter out later messages. + @type end: L{datetime} + + @param with_jid: JID against which to match messages. + @type with_jid: L{JID} + + @param extra_fields: list of extra data form fields that are not defined by the + specification. + @type: C{list} + + @param formType: The type of the Data Form ('submit' or 'form'). + @type formType: C{unicode} + + @return: XEP-0004 Data Form object. + @rtype: L{Form} + """ + form = data_form.Form(formType, formNamespace=NS_MAM) + + if formType == 'form': + for kwargs in MAMService._legacyFilters.values(): + form.addField(data_form.Field(**kwargs)) + elif formType == 'submit': + if start: + form.addField(data_form.Field(var='start', value=datetime2utc(start))) + if end: + form.addField(data_form.Field(var='end', value=datetime2utc(end))) + if with_jid: + form.addField(data_form.Field(fieldType='jid-single', var='with', value=with_jid.full())) + + if extra_fields is not None: + for field in extra_fields: + form.addField(field) + + return form diff -r 7430d1f6db22 -r c8cb4e867897 sat_tmp/wokkel/pubsub.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat_tmp/wokkel/pubsub.py Thu Nov 02 22:50:59 2017 +0100 @@ -0,0 +1,1717 @@ +# -*- coding: utf-8 -*- +# -*- test-case-name: wokkel.test.test_pubsub -*- +# +# SàT adaptation for wokkel.pubsub +# Copyright (C) 2015 Adien Cossa (souliane@mailoo.org) +# Copyright (c) 2003-2012 Ralph Meijer. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# -- + +# This program is based on wokkel (https://wokkel.ik.nu/), +# originaly written by Ralph Meijer +# It is sublicensed under AGPL v3 (or any later version) as allowed by the original +# license. + +# -- + +# Here is a copy of the original license: + +# Copyright (c) 2003-2012 Ralph Meijer. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +XMPP publish-subscribe protocol. + +This protocol is specified in +U{XEP-0060}. +""" + +from zope.interface import implements + +from twisted.internet import defer +from twisted.python import log +from twisted.words.protocols.jabber import jid, error +from twisted.words.xish import domish + +from wokkel import disco, data_form, generic, shim +from wokkel.compat import IQ +from wokkel.subprotocols import IQHandlerMixin, XMPPHandler +from wokkel.iwokkel import IPubSubClient, IPubSubService, IPubSubResource + +# Iq get and set XPath queries +IQ_GET = '/iq[@type="get"]' +IQ_SET = '/iq[@type="set"]' + +# Publish-subscribe namespaces +NS_PUBSUB = 'http://jabber.org/protocol/pubsub' +NS_PUBSUB_EVENT = NS_PUBSUB + '#event' +NS_PUBSUB_ERRORS = NS_PUBSUB + '#errors' +NS_PUBSUB_OWNER = NS_PUBSUB + "#owner" +NS_PUBSUB_NODE_CONFIG = NS_PUBSUB + "#node_config" +NS_PUBSUB_META_DATA = NS_PUBSUB + "#meta-data" +NS_PUBSUB_SUBSCRIBE_OPTIONS = NS_PUBSUB + "#subscribe_options" + +# XPath to match pubsub requests +PUBSUB_REQUEST = '/iq[@type="get" or @type="set"]/' + \ + 'pubsub[@xmlns="' + NS_PUBSUB + '" or ' + \ + '@xmlns="' + NS_PUBSUB_OWNER + '"]' + +BOOL_TRUE = ('1','true') +BOOL_FALSE = ('0','false') + +class SubscriptionPending(Exception): + """ + Raised when the requested subscription is pending acceptance. + """ + + + +class SubscriptionUnconfigured(Exception): + """ + Raised when the requested subscription needs to be configured before + becoming active. + """ + + + +class PubSubError(error.StanzaError): + """ + Exception with publish-subscribe specific condition. + """ + def __init__(self, condition, pubsubCondition, feature=None, text=None): + appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition)) + if feature: + appCondition['feature'] = feature + error.StanzaError.__init__(self, condition, + text=text, + appCondition=appCondition) + + + +class BadRequest(error.StanzaError): + """ + Bad request stanza error. + """ + def __init__(self, pubsubCondition=None, text=None): + if pubsubCondition: + appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition)) + else: + appCondition = None + error.StanzaError.__init__(self, 'bad-request', + text=text, + appCondition=appCondition) + + + +class Unsupported(PubSubError): + def __init__(self, feature, text=None): + self.feature = feature + PubSubError.__init__(self, 'feature-not-implemented', + 'unsupported', + feature, + text) + + def __str__(self): + message = PubSubError.__str__(self) + message += ', feature %r' % self.feature + return message + + +class Subscription(object): + """ + A subscription to a node. + + @ivar nodeIdentifier: The identifier of the node subscribed to. The root + node is denoted by C{None}. + @type nodeIdentifier: C{unicode} + + @ivar subscriber: The subscribing entity. + @type subscriber: L{jid.JID} + + @ivar state: The subscription state. One of C{'subscribed'}, C{'pending'}, + C{'unconfigured'}, C{'none'}. + @type state: C{unicode} + + @ivar options: Optional list of subscription options. + @type options: C{dict} + + @ivar subscriptionIdentifier: Optional subscription identifier. + @type subscriptionIdentifier: C{unicode} + """ + + def __init__(self, nodeIdentifier, subscriber, state, options=None, + subscriptionIdentifier=None): + self.nodeIdentifier = nodeIdentifier + self.subscriber = subscriber + self.state = state + self.options = options or {} + self.subscriptionIdentifier = subscriptionIdentifier + + + @staticmethod + def fromElement(element): + return Subscription( + element.getAttribute('node'), + jid.JID(element.getAttribute('jid')), + element.getAttribute('subscription'), + subscriptionIdentifier=element.getAttribute('subid')) + + + def toElement(self, defaultUri=None): + """ + Return the DOM representation of this subscription. + + @rtype: L{domish.Element} + """ + element = domish.Element((defaultUri, 'subscription')) + if self.nodeIdentifier: + element['node'] = self.nodeIdentifier + element['jid'] = unicode(self.subscriber) + element['subscription'] = self.state + if self.subscriptionIdentifier: + element['subid'] = self.subscriptionIdentifier + return element + + + +class Item(domish.Element): + """ + Publish subscribe item. + + This behaves like an object providing L{domish.IElement}. + + Item payload can be added using C{addChild} or C{addRawXml}, or using the + C{payload} keyword argument to C{__init__}. + """ + + def __init__(self, id=None, payload=None): + """ + @param id: optional item identifier + @type id: C{unicode} + @param payload: optional item payload. Either as a domish element, or + as serialized XML. + @type payload: object providing L{domish.IElement} or C{unicode}. + """ + + domish.Element.__init__(self, (None, 'item')) + if id is not None: + self['id'] = id + if payload is not None: + if isinstance(payload, basestring): + self.addRawXml(payload) + else: + self.addChild(payload) + + + +class PubSubRequest(generic.Stanza): + """ + A publish-subscribe request. + + The set of instance variables used depends on the type of request. If + a variable is not applicable or not passed in the request, its value is + C{None}. + + @ivar verb: The type of publish-subscribe request. See C{_requestVerbMap}. + @type verb: C{str}. + + @ivar affiliations: Affiliations to be modified. + @type affiliations: C{set} + + @ivar items: The items to be published, as L{domish.Element}s. + @type items: C{list} + + @ivar itemIdentifiers: Identifiers of the items to be retrieved or + retracted. + @type itemIdentifiers: C{set} + + @ivar maxItems: Maximum number of items to retrieve. + @type maxItems: C{int}. + + @ivar nodeIdentifier: Identifier of the node the request is about. + @type nodeIdentifier: C{unicode} + + @ivar nodeType: The type of node that should be created, or for which the + configuration is retrieved. C{'leaf'} or C{'collection'}. + @type nodeType: C{str} + + @ivar options: Configurations options for nodes, subscriptions and publish + requests. + @type options: L{data_form.Form} + + @ivar subscriber: The subscribing entity. + @type subscriber: L{JID} + + @ivar subscriptionIdentifier: Identifier for a specific subscription. + @type subscriptionIdentifier: C{unicode} + + @ivar subscriptions: Subscriptions to be modified, as a set of + L{Subscription}. + @type subscriptions: C{set} + + @ivar affiliations: Affiliations to be modified, as a dictionary of entity + (L{JID} to affiliation + (C{unicode}). + @type affiliations: C{dict} + """ + + verb = None + + items = None + itemIdentifiers = None + maxItems = None + nodeIdentifier = None + nodeType = None + options = None + subscriber = None + subscriptionIdentifier = None + subscriptions = None + affiliations = None + notify = None + + # Map request iq type and subelement name to request verb + _requestVerbMap = { + ('set', NS_PUBSUB, 'publish'): 'publish', + ('set', NS_PUBSUB, 'subscribe'): 'subscribe', + ('set', NS_PUBSUB, 'unsubscribe'): 'unsubscribe', + ('get', NS_PUBSUB, 'options'): 'optionsGet', + ('set', NS_PUBSUB, 'options'): 'optionsSet', + ('get', NS_PUBSUB, 'subscriptions'): 'subscriptions', + ('get', NS_PUBSUB, 'affiliations'): 'affiliations', + ('set', NS_PUBSUB, 'create'): 'create', + ('get', NS_PUBSUB_OWNER, 'default'): 'default', + ('get', NS_PUBSUB_OWNER, 'configure'): 'configureGet', + ('set', NS_PUBSUB_OWNER, 'configure'): 'configureSet', + ('get', NS_PUBSUB, 'items'): 'items', + ('set', NS_PUBSUB, 'retract'): 'retract', + ('set', NS_PUBSUB_OWNER, 'purge'): 'purge', + ('set', NS_PUBSUB_OWNER, 'delete'): 'delete', + ('get', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsGet', + ('set', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsSet', + ('get', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsGet', + ('set', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsSet', + } + + # Map request verb to request iq type and subelement name + _verbRequestMap = dict(((v, k) for k, v in _requestVerbMap.iteritems())) + + # Map request verb to parameter handler names + _parameters = { + 'publish': ['node', 'items'], + 'subscribe': ['nodeOrEmpty', 'jid', 'optionsWithSubscribe'], + 'unsubscribe': ['nodeOrEmpty', 'jid', 'subidOrNone'], + 'optionsGet': ['nodeOrEmpty', 'jid', 'subidOrNone'], + 'optionsSet': ['nodeOrEmpty', 'jid', 'options', 'subidOrNone'], + 'subscriptions': ['nodeOrEmpty'], + 'affiliations': ['nodeOrNone'], + 'create': ['nodeOrNone', 'configureOrNone'], + 'default': ['default'], + 'configureGet': ['nodeOrEmpty'], + 'configureSet': ['nodeOrEmpty', 'configureOrNone'], + 'items': ['node', 'maxItems', 'itemIdentifiers', 'subidOrNone'], + 'retract': ['node', 'notify', 'itemIdentifiers'], + 'purge': ['node'], + 'delete': ['node'], + 'affiliationsGet': ['node'], + 'affiliationsSet': ['node', 'affiliations'], + 'subscriptionsGet': ['node'], + 'subscriptionsSet': ['node', 'subscriptions'], + } + + def __init__(self, verb=None): + self.verb = verb + + + def _parse_node(self, verbElement): + """ + Parse the required node identifier out of the verbElement. + """ + try: + self.nodeIdentifier = verbElement["node"] + except KeyError: + raise BadRequest('nodeid-required') + + + def _render_node(self, verbElement): + """ + Render the required node identifier on the verbElement. + """ + if not self.nodeIdentifier: + raise Exception("Node identifier is required") + + verbElement['node'] = self.nodeIdentifier + + + def _parse_nodeOrEmpty(self, verbElement): + """ + Parse the node identifier out of the verbElement. May be empty. + """ + self.nodeIdentifier = verbElement.getAttribute("node", '') + + + def _render_nodeOrEmpty(self, verbElement): + """ + Render the node identifier on the verbElement. May be empty. + """ + if self.nodeIdentifier: + verbElement['node'] = self.nodeIdentifier + + + def _parse_nodeOrNone(self, verbElement): + """ + Parse the optional node identifier out of the verbElement. + """ + self.nodeIdentifier = verbElement.getAttribute("node") + + + def _render_nodeOrNone(self, verbElement): + """ + Render the optional node identifier on the verbElement. + """ + if self.nodeIdentifier: + verbElement['node'] = self.nodeIdentifier + + + def _parse_items(self, verbElement): + """ + Parse items out of the verbElement for publish requests. + """ + self.items = [] + for element in verbElement.elements(): + if element.uri == NS_PUBSUB and element.name == 'item': + self.items.append(element) + + + def _render_items(self, verbElement): + """ + Render items into the verbElement for publish requests. + """ + if self.items: + for item in self.items: + item.uri = NS_PUBSUB + verbElement.addChild(item) + + + def _parse_jid(self, verbElement): + """ + Parse subscriber out of the verbElement for un-/subscribe requests. + """ + try: + self.subscriber = jid.internJID(verbElement["jid"]) + except KeyError: + raise BadRequest('jid-required') + + + def _render_jid(self, verbElement): + """ + Render subscriber into the verbElement for un-/subscribe requests. + """ + verbElement['jid'] = self.subscriber.full() + + + def _parse_default(self, verbElement): + """ + Parse node type out of a request for the default node configuration. + """ + form = data_form.findForm(verbElement, NS_PUBSUB_NODE_CONFIG) + if form is not None and form.formType == 'submit': + values = form.getValues() + self.nodeType = values.get('pubsub#node_type', 'leaf') + else: + self.nodeType = 'leaf' + + + def _parse_configure(self, verbElement): + """ + Parse options out of a request for setting the node configuration. + """ + form = data_form.findForm(verbElement, NS_PUBSUB_NODE_CONFIG) + if form is not None: + if form.formType in ('submit', 'cancel'): + self.options = form + else: + raise BadRequest(text=u"Unexpected form type '%s'" % form.formType) + else: + raise BadRequest(text="Missing configuration form") + + + def _parse_configureOrNone(self, verbElement): + """ + Parse optional node configuration form in create request. + """ + for element in verbElement.parent.elements(): + if element.uri in (NS_PUBSUB, NS_PUBSUB_OWNER) and element.name == 'configure': + form = data_form.findForm(element, NS_PUBSUB_NODE_CONFIG) + if form is not None: + if form.formType != 'submit': + raise BadRequest(text=u"Unexpected form type '%s'" % + form.formType) + else: + form = data_form.Form('submit', + formNamespace=NS_PUBSUB_NODE_CONFIG) + self.options = form + + + def _render_configureOrNone(self, verbElement): + """ + Render optional node configuration form in create request. + """ + if self.options is not None: + if verbElement.name == 'configure': + configure = verbElement + else: + configure = verbElement.parent.addElement('configure') + configure.addChild(self.options.toElement()) + + + def _parse_itemIdentifiers(self, verbElement): + """ + Parse item identifiers out of items and retract requests. + """ + self.itemIdentifiers = [] + for element in verbElement.elements(): + if element.uri == NS_PUBSUB and element.name == 'item': + try: + self.itemIdentifiers.append(element["id"]) + except KeyError: + raise BadRequest() + + + def _render_itemIdentifiers(self, verbElement): + """ + Render item identifiers into items and retract requests. + """ + if self.itemIdentifiers: + for itemIdentifier in self.itemIdentifiers: + item = verbElement.addElement('item') + item['id'] = itemIdentifier + + + def _parse_maxItems(self, verbElement): + """ + Parse maximum items out of an items request. + """ + value = verbElement.getAttribute('max_items') + + if value: + try: + self.maxItems = int(value) + except ValueError: + raise BadRequest(text="Field max_items requires a positive " + + "integer value") + + + def _render_maxItems(self, verbElement): + """ + Render maximum items into an items request. + """ + if self.maxItems: + verbElement['max_items'] = unicode(self.maxItems) + + + def _parse_subidOrNone(self, verbElement): + """ + Parse subscription identifier out of a request. + """ + self.subscriptionIdentifier = verbElement.getAttribute("subid") + + + def _render_subidOrNone(self, verbElement): + """ + Render subscription identifier into a request. + """ + if self.subscriptionIdentifier: + verbElement['subid'] = self.subscriptionIdentifier + + + def _parse_options(self, verbElement): + """ + Parse options form out of a subscription options request. + """ + form = data_form.findForm(verbElement, NS_PUBSUB_SUBSCRIBE_OPTIONS) + if form is not None: + if form.formType in ('submit', 'cancel'): + self.options = form + else: + raise BadRequest(text=u"Unexpected form type '%s'" % form.formType) + else: + raise BadRequest(text="Missing options form") + + + def _render_options(self, verbElement): + verbElement.addChild(self.options.toElement()) + + + def _parse_optionsWithSubscribe(self, verbElement): + for element in verbElement.parent.elements(): + if element.name == 'options' and element.uri == NS_PUBSUB: + form = data_form.findForm(element, + NS_PUBSUB_SUBSCRIBE_OPTIONS) + if form is not None: + if form.formType != 'submit': + raise BadRequest(text=u"Unexpected form type '%s'" % + form.formType) + else: + form = data_form.Form('submit', + formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) + self.options = form + + + def _render_optionsWithSubscribe(self, verbElement): + if self.options is not None: + optionsElement = verbElement.parent.addElement('options') + self._render_options(optionsElement) + + + def _parse_affiliations(self, verbElement): + self.affiliations = {} + for element in verbElement.elements(): + if (element.uri == NS_PUBSUB_OWNER and + element.name == 'affiliation'): + try: + entity = jid.internJID(element['jid']).userhostJID() + except KeyError: + raise BadRequest(text='Missing jid attribute') + + if entity in self.affiliations: + raise BadRequest(text='Multiple affiliations for an entity') + + try: + affiliation = element['affiliation'] + except KeyError: + raise BadRequest(text='Missing affiliation attribute') + + self.affiliations[entity] = affiliation + + + def _render_affiliations(self, verbElement): + for entity, affiliation in self.affiliations.iteritems(): + affiliationElement = verbElement.addElement((NS_PUBSUB_OWNER, 'affiliation')) + affiliationElement['jid'] = entity.full() + affiliationElement['affiliation'] = affiliation + + + def _parse_subscriptions(self, verbElement): + self.subscriptions = set() + seen_entities = set() + for element in verbElement.elements(): + if (element.uri == NS_PUBSUB_OWNER and + element.name == 'subscription'): + try: + subscriber = jid.internJID(element['jid']).userhostJID() + except KeyError: + raise BadRequest(text='Missing jid attribute') + + if subscriber in seen_entities: + raise BadRequest(text='Multiple subscriptions for an subscriber') + seen_entities.add(subscriber) + + try: + state = element['subscription'] + except KeyError: + # §8.8.2.1 says that value MUST NOT be changed + # if subscription is missing + continue + + self.subscriptions.add(Subscription(self.nodeIdentifier, + subscriber, + state)) + + + def _render_subscriptions(self, verbElement): + for subscription in self.subscriptions: + subscriptionElement = verbElement.addElement((NS_PUBSUB_OWNER, 'subscription')) + subscriptionElement['jid'] = subscription.subscriber.full() + subscriptionElement['subscription'] = subscription.state + + + def _parse_notify(self, verbElement): + value = verbElement.getAttribute('notify') + + if value: + if value in BOOL_TRUE: + self.notify = True + elif value in BOOL_FALSE: + self.notify = False + else: + raise BadRequest(text="Field notify must be a boolean value") + + + def _render_notify(self, verbElement): + if self.notify is not None: + verbElement['notify'] = "true" if self.notify else "false" + + + def parseElement(self, element): + """ + Parse the publish-subscribe verb and parameters out of a request. + """ + generic.Stanza.parseElement(self, element) + + verbs = [] + verbElements = [] + for child in element.pubsub.elements(): + key = (self.stanzaType, child.uri, child.name) + try: + verb = self._requestVerbMap[key] + except KeyError: + continue + + verbs.append(verb) + verbElements.append(child) + + if not verbs: + raise NotImplementedError() + + if len(verbs) > 1: + if 'optionsSet' in verbs and 'subscribe' in verbs: + self.verb = 'subscribe' + verbElement = verbElements[verbs.index('subscribe')] + else: + raise NotImplementedError() + else: + self.verb = verbs[0] + verbElement = verbElements[0] + + for parameter in self._parameters[self.verb]: + getattr(self, '_parse_%s' % parameter)(verbElement) + + + + def send(self, xs): + """ + Send this request to its recipient. + + This renders all of the relevant parameters for this specific + requests into an L{IQ}, and invoke its C{send} method. + This returns a deferred that fires upon reception of a response. See + L{IQ} for details. + + @param xs: The XML stream to send the request on. + @type xs: L{twisted.words.protocols.jabber.xmlstream.XmlStream} + @rtype: L{defer.Deferred}. + """ + + try: + (self.stanzaType, + childURI, + childName) = self._verbRequestMap[self.verb] + except KeyError: + raise NotImplementedError() + + iq = IQ(xs, self.stanzaType) + iq.addElement((childURI, 'pubsub')) + verbElement = iq.pubsub.addElement(childName) + + if self.sender: + iq['from'] = self.sender.full() + if self.recipient: + iq['to'] = self.recipient.full() + + for parameter in self._parameters[self.verb]: + getattr(self, '_render_%s' % parameter)(verbElement) + + return iq.send() + + + +class PubSubEvent(object): + """ + A publish subscribe event. + + @param sender: The entity from which the notification was received. + @type sender: L{jid.JID} + @param recipient: The entity to which the notification was sent. + @type recipient: L{wokkel.pubsub.ItemsEvent} + @param nodeIdentifier: Identifier of the node the event pertains to. + @type nodeIdentifier: C{unicode} + @param headers: SHIM headers, see L{wokkel.shim.extractHeaders}. + @type headers: C{dict} + """ + + def __init__(self, sender, recipient, nodeIdentifier, headers): + self.sender = sender + self.recipient = recipient + self.nodeIdentifier = nodeIdentifier + self.headers = headers + + + +class ItemsEvent(PubSubEvent): + """ + A publish-subscribe event that signifies new, updated and retracted items. + + @param items: List of received items as domish elements. + @type items: C{list} of L{domish.Element} + """ + + def __init__(self, sender, recipient, nodeIdentifier, items, headers): + PubSubEvent.__init__(self, sender, recipient, nodeIdentifier, headers) + self.items = items + + + +class DeleteEvent(PubSubEvent): + """ + A publish-subscribe event that signifies the deletion of a node. + """ + + redirectURI = None + + + +class PurgeEvent(PubSubEvent): + """ + A publish-subscribe event that signifies the purging of a node. + """ + + + +class PubSubClient(XMPPHandler): + """ + Publish subscribe client protocol. + """ + implements(IPubSubClient) + + _request_class = PubSubRequest + + def connectionInitialized(self): + self.xmlstream.addObserver('/message/event[@xmlns="%s"]' % + NS_PUBSUB_EVENT, self._onEvent) + + + def _onEvent(self, message): + if message.getAttribute('type') == 'error': + return + + try: + sender = jid.JID(message["from"]) + recipient = jid.JID(message["to"]) + except KeyError: + return + + actionElement = None + for element in message.event.elements(): + if element.uri == NS_PUBSUB_EVENT: + actionElement = element + + if not actionElement: + return + + eventHandler = getattr(self, "_onEvent_%s" % actionElement.name, None) + + if eventHandler: + headers = shim.extractHeaders(message) + eventHandler(sender, recipient, actionElement, headers) + message.handled = True + + + def _onEvent_items(self, sender, recipient, action, headers): + nodeIdentifier = action["node"] + + items = [element for element in action.elements() + if element.name in ('item', 'retract')] + + event = ItemsEvent(sender, recipient, nodeIdentifier, items, headers) + self.itemsReceived(event) + + + def _onEvent_delete(self, sender, recipient, action, headers): + nodeIdentifier = action["node"] + event = DeleteEvent(sender, recipient, nodeIdentifier, headers) + if action.redirect: + event.redirectURI = action.redirect.getAttribute('uri') + self.deleteReceived(event) + + + def _onEvent_purge(self, sender, recipient, action, headers): + nodeIdentifier = action["node"] + event = PurgeEvent(sender, recipient, nodeIdentifier, headers) + self.purgeReceived(event) + + + def itemsReceived(self, event): + pass + + + def deleteReceived(self, event): + pass + + + def purgeReceived(self, event): + pass + + + def createNode(self, service, nodeIdentifier=None, options=None, + sender=None): + """ + Create a publish subscribe node. + + @param service: The publish subscribe service to create the node at. + @type service: L{JID} + @param nodeIdentifier: Optional suggestion for the id of the node. + @type nodeIdentifier: C{unicode} + @param options: Optional node configuration options. + @type options: C{dict} + """ + request = self._request_class('create') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + request.sender = sender + + if options: + form = data_form.Form(formType='submit', + formNamespace=NS_PUBSUB_NODE_CONFIG) + form.makeFields(options) + request.options = form + + def cb(iq): + try: + new_node = iq.pubsub.create["node"] + except AttributeError: + # the suggested node identifier was accepted + new_node = nodeIdentifier + return new_node + + d = request.send(self.xmlstream) + d.addCallback(cb) + return d + + + def deleteNode(self, service, nodeIdentifier, sender=None): + """ + Delete a publish subscribe node. + + @param service: The publish subscribe service to delete the node from. + @type service: L{JID} + @param nodeIdentifier: The identifier of the node. + @type nodeIdentifier: C{unicode} + """ + request = self._request_class('delete') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + request.sender = sender + return request.send(self.xmlstream) + + + def subscribe(self, service, nodeIdentifier, subscriber, + options=None, sender=None): + """ + Subscribe to a publish subscribe node. + + @param service: The publish subscribe service that keeps the node. + @type service: L{JID} + + @param nodeIdentifier: The identifier of the node. + @type nodeIdentifier: C{unicode} + + @param subscriber: The entity to subscribe to the node. This entity + will get notifications of new published items. + @type subscriber: L{JID} + + @param options: Subscription options. + @type options: C{dict} + + @return: Deferred that fires with L{Subscription} or errbacks with + L{SubscriptionPending} or L{SubscriptionUnconfigured}. + @rtype: L{defer.Deferred} + """ + request = self._request_class('subscribe') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + request.subscriber = subscriber + request.sender = sender + + if options: + form = data_form.Form(formType='submit', + formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) + form.makeFields(options) + request.options = form + + def cb(iq): + subscription = Subscription.fromElement(iq.pubsub.subscription) + + if subscription.state == 'pending': + raise SubscriptionPending() + elif subscription.state == 'unconfigured': + raise SubscriptionUnconfigured() + else: + # we assume subscription == 'subscribed' + # any other value would be invalid, but that should have + # yielded a stanza error. + return subscription + + d = request.send(self.xmlstream) + d.addCallback(cb) + return d + + + def unsubscribe(self, service, nodeIdentifier, subscriber, + subscriptionIdentifier=None, sender=None): + """ + Unsubscribe from a publish subscribe node. + + @param service: The publish subscribe service that keeps the node. + @type service: L{JID} + + @param nodeIdentifier: The identifier of the node. + @type nodeIdentifier: C{unicode} + + @param subscriber: The entity to unsubscribe from the node. + @type subscriber: L{JID} + + @param subscriptionIdentifier: Optional subscription identifier. + @type subscriptionIdentifier: C{unicode} + """ + request = self._request_class('unsubscribe') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + request.subscriber = subscriber + request.subscriptionIdentifier = subscriptionIdentifier + request.sender = sender + return request.send(self.xmlstream) + + + def publish(self, service, nodeIdentifier, items=None, sender=None): + """ + Publish to a publish subscribe node. + + @param service: The publish subscribe service that keeps the node. + @type service: L{JID} + @param nodeIdentifier: The identifier of the node. + @type nodeIdentifier: C{unicode} + @param items: Optional list of L{Item}s to publish. + @type items: C{list} + """ + request = self._request_class('publish') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + request.items = items + request.sender = sender + return request.send(self.xmlstream) + + + def items(self, service, nodeIdentifier, maxItems=None, itemIdentifiers=None, + subscriptionIdentifier=None, sender=None): + """ + Retrieve previously published items from a publish subscribe node. + + @param service: The publish subscribe service that keeps the node. + @type service: L{JID} + + @param nodeIdentifier: The identifier of the node. + @type nodeIdentifier: C{unicode} + + @param maxItems: Optional limit on the number of retrieved items. + @type maxItems: C{int} + + @param itemIdentifiers: Identifiers of the items to be retrieved. + @type itemIdentifiers: C{set} + + @param subscriptionIdentifier: Optional subscription identifier. In + case the node has been subscribed to multiple times, this narrows + the results to the specific subscription. + @type subscriptionIdentifier: C{unicode} + """ + request = self._request_class('items') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + if maxItems: + request.maxItems = str(int(maxItems)) + request.subscriptionIdentifier = subscriptionIdentifier + request.sender = sender + request.itemIdentifiers = itemIdentifiers + + def cb(iq): + items = [] + for element in iq.pubsub.items.elements(): + if element.uri == NS_PUBSUB and element.name == 'item': + items.append(element) + return items + + d = request.send(self.xmlstream) + d.addCallback(cb) + return d + + def retractItems(self, service, nodeIdentifier, itemIdentifiers, notify=None, sender=None): + """ + Retract items from a publish subscribe node. + + @param service: The publish subscribe service to delete the node from. + @type service: L{JID} + @param nodeIdentifier: The identifier of the node. + @type nodeIdentifier: C{unicode} + @param itemIdentifiers: Identifiers of the items to be retracted. + @type itemIdentifiers: C{set} + @param notify: True if notification is required + @type notify: C{unicode} + """ + request = self._request_class('retract') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + request.itemIdentifiers = itemIdentifiers + request.notify = notify + request.sender = sender + return request.send(self.xmlstream) + + def getOptions(self, service, nodeIdentifier, subscriber, + subscriptionIdentifier=None, sender=None): + """ + Get subscription options. + + @param service: The publish subscribe service that keeps the node. + @type service: L{JID} + + @param nodeIdentifier: The identifier of the node. + @type nodeIdentifier: C{unicode} + + @param subscriber: The entity subscribed to the node. + @type subscriber: L{JID} + + @param subscriptionIdentifier: Optional subscription identifier. + @type subscriptionIdentifier: C{unicode} + + @rtype: L{data_form.Form} + """ + request = self._request_class('optionsGet') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + request.subscriber = subscriber + request.subscriptionIdentifier = subscriptionIdentifier + request.sender = sender + + def cb(iq): + form = data_form.findForm(iq.pubsub.options, + NS_PUBSUB_SUBSCRIBE_OPTIONS) + form.typeCheck() + return form + + d = request.send(self.xmlstream) + d.addCallback(cb) + return d + + + def setOptions(self, service, nodeIdentifier, subscriber, + options, subscriptionIdentifier=None, sender=None): + """ + Set subscription options. + + @param service: The publish subscribe service that keeps the node. + @type service: L{JID} + + @param nodeIdentifier: The identifier of the node. + @type nodeIdentifier: C{unicode} + + @param subscriber: The entity subscribed to the node. + @type subscriber: L{JID} + + @param options: Subscription options. + @type options: C{dict}. + + @param subscriptionIdentifier: Optional subscription identifier. + @type subscriptionIdentifier: C{unicode} + """ + request = self._request_class('optionsSet') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + request.subscriber = subscriber + request.subscriptionIdentifier = subscriptionIdentifier + request.sender = sender + + form = data_form.Form(formType='submit', + formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) + form.makeFields(options) + request.options = form + + d = request.send(self.xmlstream) + return d + + + +class PubSubService(XMPPHandler, IQHandlerMixin): + """ + Protocol implementation for a XMPP Publish Subscribe Service. + + The word Service here is used as taken from the Publish Subscribe + specification. It is the party responsible for keeping nodes and their + subscriptions, and sending out notifications. + + Methods from the L{IPubSubService} interface that are called as a result + of an XMPP request may raise exceptions. Alternatively the deferred + returned by these methods may have their errback called. These are handled + as follows: + + - If the exception is an instance of L{error.StanzaError}, an error + response iq is returned. + - Any other exception is reported using L{log.msg}. An error response + with the condition C{internal-server-error} is returned. + + The default implementation of said methods raises an L{Unsupported} + exception and are meant to be overridden. + + @ivar discoIdentity: Service discovery identity as a dictionary with + keys C{'category'}, C{'type'} and C{'name'}. + @ivar pubSubFeatures: List of supported publish-subscribe features for + service discovery, as C{str}. + @type pubSubFeatures: C{list} or C{None} + """ + + implements(IPubSubService, disco.IDisco) + + iqHandlers = { + '/*': '_onPubSubRequest', + } + + _legacyHandlers = { + 'publish': ('publish', ['sender', 'recipient', + 'nodeIdentifier', 'items']), + 'subscribe': ('subscribe', ['sender', 'recipient', + 'nodeIdentifier', 'subscriber']), + 'unsubscribe': ('unsubscribe', ['sender', 'recipient', + 'nodeIdentifier', 'subscriber']), + 'subscriptions': ('subscriptions', ['sender', 'recipient']), + 'affiliations': ('affiliations', ['sender', 'recipient']), + 'create': ('create', ['sender', 'recipient', 'nodeIdentifier']), + 'getConfigurationOptions': ('getConfigurationOptions', []), + 'default': ('getDefaultConfiguration', + ['sender', 'recipient', 'nodeType']), + 'configureGet': ('getConfiguration', ['sender', 'recipient', + 'nodeIdentifier']), + 'configureSet': ('setConfiguration', ['sender', 'recipient', + 'nodeIdentifier', 'options']), + 'items': ('items', ['sender', 'recipient', 'nodeIdentifier', + 'maxItems', 'itemIdentifiers']), + 'retract': ('retract', ['sender', 'recipient', 'nodeIdentifier', + 'itemIdentifiers']), + 'purge': ('purge', ['sender', 'recipient', 'nodeIdentifier']), + 'delete': ('delete', ['sender', 'recipient', 'nodeIdentifier']), + } + + _request_class = PubSubRequest + + hideNodes = False + + def __init__(self, resource=None): + self.resource = resource + self.discoIdentity = {'category': 'pubsub', + 'type': 'service', + 'name': 'Generic Publish-Subscribe Service'} + + self.pubSubFeatures = [] + + + def connectionMade(self): + self.xmlstream.addObserver(PUBSUB_REQUEST, self.handleRequest) + + + def getDiscoInfo(self, requestor, target, nodeIdentifier=''): + def toInfo(nodeInfo): + if not nodeInfo: + return + + (nodeType, metaData) = nodeInfo['type'], nodeInfo['meta-data'] + info.append(disco.DiscoIdentity('pubsub', nodeType)) + if metaData: + form = data_form.Form(formType="result", + formNamespace=NS_PUBSUB_META_DATA) + form.addField( + data_form.Field( + var='pubsub#node_type', + value=nodeType, + label='The type of node (collection or leaf)' + ) + ) + + for metaDatum in metaData: + form.addField(data_form.Field.fromDict(metaDatum)) + + info.append(form) + + return + + info = [] + + request = self._request_class('discoInfo') + + if self.resource is not None: + resource = self.resource.locateResource(request) + identity = resource.discoIdentity + features = resource.features + getInfo = resource.getInfo + else: + category = self.discoIdentity['category'] + idType = self.discoIdentity['type'] + name = self.discoIdentity['name'] + identity = disco.DiscoIdentity(category, idType, name) + features = self.pubSubFeatures + getInfo = self.getNodeInfo + + if not nodeIdentifier: + info.append(identity) + info.append(disco.DiscoFeature(disco.NS_DISCO_ITEMS)) + info.extend([disco.DiscoFeature("%s#%s" % (NS_PUBSUB, feature)) + for feature in features]) + + d = defer.maybeDeferred(getInfo, requestor, target, nodeIdentifier or '') + d.addCallback(toInfo) + d.addErrback(log.err) + d.addCallback(lambda _: info) + return d + + + def _parseNodes(self, nodes, target): + """parse return values of resource.getNodes + + basestring values are used as node + tuple are unpacked as node, name + """ + items = [] + for node in nodes: + if isinstance(node, basestring): + items.append(disco.DiscoItem(target, node)) + else: + _node, name = node + items.append(disco.DiscoItem(target, _node, name)) + return items + + + def getDiscoItems(self, requestor, target, nodeIdentifier=''): + if self.hideNodes: + d = defer.succeed([]) + elif self.resource is not None: + request = self._request_class('discoInfo') + resource = self.resource.locateResource(request) + d = resource.getNodes(requestor, target, nodeIdentifier) + elif nodeIdentifier: + d = self.getNodes(requestor, target) + else: + d = defer.succeed([]) + + d.addCallback(self._parseNodes, target) + return d + + + def _onPubSubRequest(self, iq): + request = self._request_class.fromElement(iq) + if self.resource is not None: + resource = self.resource.locateResource(request) + else: + resource = self + + # Preprocess the request, knowing the handling resource + try: + preProcessor = getattr(self, '_preProcess_%s' % request.verb) + except AttributeError: + pass + else: + request = preProcessor(resource, request) + if request is None: + return defer.succeed(None) + + # Process the request itself, + if resource is not self: + try: + handler = getattr(resource, request.verb) + except AttributeError: + text = "Request verb: %s" % request.verb + return defer.fail(Unsupported('', text)) + + d = handler(request) + else: + try: + handlerName, argNames = self._legacyHandlers[request.verb] + except KeyError: + text = "Request verb: %s" % request.verb + return defer.fail(Unsupported('', text)) + + handler = getattr(self, handlerName) + args = [getattr(request, arg) for arg in argNames] + d = handler(*args) + + # If needed, translate the result into a response + try: + cb = getattr(self, '_toResponse_%s' % request.verb) + except AttributeError: + pass + else: + d.addCallback(cb, resource, request) + + return d + + + def _toResponse_subscribe(self, result, resource, request): + response = domish.Element((NS_PUBSUB, "pubsub")) + response.addChild(result.toElement(NS_PUBSUB)) + return response + + + def _toResponse_subscriptions(self, result, resource, request): + response = domish.Element((NS_PUBSUB, 'pubsub')) + subscriptions = response.addElement('subscriptions') + for subscription in result: + subscriptions.addChild(subscription.toElement(NS_PUBSUB)) + return response + + + def _toResponse_affiliations(self, result, resource, request): + response = domish.Element((NS_PUBSUB, 'pubsub')) + affiliations = response.addElement('affiliations') + + for nodeIdentifier, affiliation in result: + item = affiliations.addElement('affiliation') + item['node'] = nodeIdentifier + item['affiliation'] = affiliation + + return response + + + def _toResponse_create(self, result, resource, request): + if not request.nodeIdentifier or request.nodeIdentifier != result: + response = domish.Element((NS_PUBSUB, 'pubsub')) + create = response.addElement('create') + create['node'] = result + return response + else: + return None + + + def _formFromConfiguration(self, resource, values): + fieldDefs = resource.getConfigurationOptions() + form = data_form.Form(formType="form", + formNamespace=NS_PUBSUB_NODE_CONFIG) + form.makeFields(values, fieldDefs) + return form + + + def _checkConfiguration(self, resource, form): + fieldDefs = resource.getConfigurationOptions() + form.typeCheck(fieldDefs, filterUnknown=True) + + + def _preProcess_create(self, resource, request): + if request.options: + self._checkConfiguration(resource, request.options) + return request + + + def _preProcess_default(self, resource, request): + if request.nodeType not in ('leaf', 'collection'): + raise error.StanzaError('not-acceptable') + else: + return request + + + def _toResponse_default(self, options, resource, request): + response = domish.Element((NS_PUBSUB_OWNER, "pubsub")) + default = response.addElement("default") + form = self._formFromConfiguration(resource, options) + default.addChild(form.toElement()) + return response + + + def _toResponse_configureGet(self, options, resource, request): + response = domish.Element((NS_PUBSUB_OWNER, "pubsub")) + configure = response.addElement("configure") + form = self._formFromConfiguration(resource, options) + configure.addChild(form.toElement()) + + if request.nodeIdentifier: + configure["node"] = request.nodeIdentifier + + return response + + + def _preProcess_configureSet(self, resource, request): + if request.options.formType == 'cancel': + return None + else: + self._checkConfiguration(resource, request.options) + return request + + + def _toResponse_items(self, result, resource, request): + response = domish.Element((NS_PUBSUB, 'pubsub')) + items = response.addElement('items') + items["node"] = request.nodeIdentifier + + for item in result: + item.uri = NS_PUBSUB + items.addChild(item) + + return response + + + def _createNotification(self, eventType, service, nodeIdentifier, + subscriber, subscriptions=None): + headers = [] + + if subscriptions: + for subscription in subscriptions: + if nodeIdentifier != subscription.nodeIdentifier: + headers.append(('Collection', subscription.nodeIdentifier)) + + message = domish.Element((None, "message")) + message["from"] = service.full() + message["to"] = subscriber.full() + event = message.addElement((NS_PUBSUB_EVENT, "event")) + + element = event.addElement(eventType) + element["node"] = nodeIdentifier + + if headers: + message.addChild(shim.Headers(headers)) + + return message + + + def _toResponse_affiliationsGet(self, result, resource, request): + response = domish.Element((NS_PUBSUB_OWNER, 'pubsub')) + affiliations = response.addElement('affiliations') + + if request.nodeIdentifier: + affiliations['node'] = request.nodeIdentifier + + for entity, affiliation in result.iteritems(): + item = affiliations.addElement('affiliation') + item['jid'] = entity.full() + item['affiliation'] = affiliation + + return response + + + def _toResponse_subscriptionsGet(self, result, resource, request): + response = domish.Element((NS_PUBSUB, 'pubsub')) + subscriptions = response.addElement('subscriptions') + subscriptions['node'] = request.nodeIdentifier + for subscription in result: + subscription_element = subscription.toElement(NS_PUBSUB) + del subscription_element['node'] + subscriptions.addChild(subscription_element) + return response + + + # public methods + + def notifyPublish(self, service, nodeIdentifier, notifications): + for subscriber, subscriptions, items in notifications: + message = self._createNotification('items', service, + nodeIdentifier, subscriber, + subscriptions) + for item in items: + item.uri = NS_PUBSUB_EVENT + message.event.items.addChild(item) + self.send(message) + + + def notifyRetract(self, service, nodeIdentifier, notifications): + for subscriber, subscriptions, items in notifications: + message = self._createNotification('items', service, + nodeIdentifier, subscriber, + subscriptions) + for item in items: + retract = domish.Element((None, "retract")) + retract['id'] = item['id'] + message.event.items.addChild(retract) + self.send(message) + + + def notifyDelete(self, service, nodeIdentifier, subscribers, + redirectURI=None): + for subscriber in subscribers: + message = self._createNotification('delete', service, + nodeIdentifier, + subscriber) + if redirectURI: + redirect = message.event.delete.addElement('redirect') + redirect['uri'] = redirectURI + self.send(message) + + + def getNodeInfo(self, requestor, service, nodeIdentifier): + return None + + + def getNodes(self, requestor, service): + return [] + + + def publish(self, requestor, service, nodeIdentifier, items): + raise Unsupported('publish') + + + def subscribe(self, requestor, service, nodeIdentifier, subscriber): + raise Unsupported('subscribe') + + + def unsubscribe(self, requestor, service, nodeIdentifier, subscriber): + raise Unsupported('subscribe') + + + def subscriptions(self, requestor, service): + raise Unsupported('retrieve-subscriptions') + + + def affiliations(self, requestor, service): + raise Unsupported('retrieve-affiliations') + + + def create(self, requestor, service, nodeIdentifier): + raise Unsupported('create-nodes') + + + def getConfigurationOptions(self): + return {} + + + def getDefaultConfiguration(self, requestor, service, nodeType): + raise Unsupported('retrieve-default') + + + def getConfiguration(self, requestor, service, nodeIdentifier): + raise Unsupported('config-node') + + + def setConfiguration(self, requestor, service, nodeIdentifier, options): + raise Unsupported('config-node') + + + def items(self, requestor, service, nodeIdentifier, maxItems, + itemIdentifiers): + raise Unsupported('retrieve-items') + + + def retract(self, requestor, service, nodeIdentifier, itemIdentifiers): + raise Unsupported('retract-items') + + + def purge(self, requestor, service, nodeIdentifier): + raise Unsupported('purge-nodes') + + + def delete(self, requestor, service, nodeIdentifier): + raise Unsupported('delete-nodes') + + + +class PubSubResource(object): + + implements(IPubSubResource) + + features = [] + discoIdentity = disco.DiscoIdentity('pubsub', + 'service', + 'Publish-Subscribe Service') + + + def locateResource(self, request): + return self + + + def getInfo(self, requestor, service, nodeIdentifier): + return defer.succeed(None) + + + def getNodes(self, requestor, service, nodeIdentifier): + return defer.succeed([]) + + + def getConfigurationOptions(self): + return {} + + + def publish(self, request): + return defer.fail(Unsupported('publish')) + + + def subscribe(self, request): + return defer.fail(Unsupported('subscribe')) + + + def unsubscribe(self, request): + return defer.fail(Unsupported('subscribe')) + + + def subscriptions(self, request): + return defer.fail(Unsupported('retrieve-subscriptions')) + + + def affiliations(self, request): + return defer.fail(Unsupported('retrieve-affiliations')) + + + def create(self, request): + return defer.fail(Unsupported('create-nodes')) + + + def default(self, request): + return defer.fail(Unsupported('retrieve-default')) + + + def configureGet(self, request): + return defer.fail(Unsupported('config-node')) + + + def configureSet(self, request): + return defer.fail(Unsupported('config-node')) + + + def items(self, request): + return defer.fail(Unsupported('retrieve-items')) + + + def retract(self, request): + return defer.fail(Unsupported('retract-items')) + + + def purge(self, request): + return defer.fail(Unsupported('purge-nodes')) + + + def delete(self, request): + return defer.fail(Unsupported('delete-nodes')) + + + def affiliationsGet(self, request): + return defer.fail(Unsupported('retrieve-affiliations')) + + + def affiliationsSet(self, request): + return defer.fail(Unsupported('modify-affiliations')) + + + def subscriptionsGet(self, request): + return defer.fail(Unsupported('manage-subscriptions')) + + + def subscriptionsSet(self, request): + return defer.fail(Unsupported('manage-subscriptions')) diff -r 7430d1f6db22 -r c8cb4e867897 sat_tmp/wokkel/rsm.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat_tmp/wokkel/rsm.py Thu Nov 02 22:50:59 2017 +0100 @@ -0,0 +1,425 @@ +# -*- coding: utf-8 -*- +# -*- test-case-name: wokkel.test.test_rsm -*- +# +# SàT Wokkel extension for Result Set Management (XEP-0059) +# Copyright (C) 2015 Adien Cossa (souliane@mailoo.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +""" +XMPP Result Set Management protocol. + +This protocol is specified in +U{XEP-0059}. +""" + +from twisted.words.xish import domish +from twisted.words.protocols.jabber import error + +import pubsub +import copy + + +NS_RSM = 'http://jabber.org/protocol/rsm' + + +class RSMError(error.StanzaError): + """ + RSM error. + """ + def __init__(self, text=None): + error.StanzaError.__init__(self, 'bad-request', text=text) + + +class RSMNotFoundError(Exception): + """ + An expected RSM element has not been found. + """ + + +class RSMRequest(object): + """ + A Result Set Management request. + + @ivar max_: limit on the number of retrieved items. + @itype max_: C{int} or C{unicode} + + @ivar index: starting index of the requested page. + @itype index: C{int} or C{unicode} or C{None} + + @ivar after: ID of the element immediately preceding the page. + @itype after: C{unicode} + + @ivar before: ID of the element immediately following the page. + @itype before: C{unicode} + """ + + def __init__(self, max_=10, after=None, before=None, index=None): + self.max = int(max_) + + if index is not None: + assert after is None and before is None + index = int(index) + self.index = index + + if after is not None: + assert before is None + assert isinstance(after, basestring) + self.after = after + + if before is not None: + assert isinstance(before, basestring) + self.before = before + + def __str__(self): + return "RSM Request: max={0.max} after={0.after} before={0.before} index={0.index}".format(self) + + @classmethod + def fromElement(cls, element): + """Parse the given request element. + + @param element: request containing a set element, or set element itself. + @type element: L{domish.Element} + + @return: RSMRequest instance. + @rtype: L{RSMRequest} + """ + + if element.name == 'set' and element.uri == NS_RSM: + set_elt = element + else: + try: + set_elt = element.elements(NS_RSM, 'set').next() + except StopIteration: + raise RSMNotFoundError() + + try: + before_elt = set_elt.elements(NS_RSM, 'before').next() + except StopIteration: + before = None + else: + before = unicode(before_elt) + + try: + after_elt = set_elt.elements(NS_RSM, 'after').next() + except StopIteration: + after = None + else: + after = unicode(after_elt) + if not after: + raise RSMError(" element can't be empty in RSM request") + + try: + max_elt = set_elt.elements(NS_RSM, 'max').next() + except StopIteration: + # FIXME: even if it doesn't make a lot of sense without it + # element is not mandatory in XEP-0059 + raise RSMError("RSM request is missing its 'max' element") + else: + try: + max_ = int(unicode(max_elt)) + except ValueError: + raise RSMError("bad value for 'max' element") + + try: + index_elt = set_elt.elements(NS_RSM, 'index').next() + except StopIteration: + index = None + else: + try: + index = int(unicode(index_elt)) + except ValueError: + raise RSMError("bad value for 'index' element") + + return RSMRequest(max_, after, before, index) + + def toElement(self): + """ + Return the DOM representation of this RSM request. + + @rtype: L{domish.Element} + """ + set_elt = domish.Element((NS_RSM, 'set')) + set_elt.addElement('max', content=unicode(self.max)) + + if self.index is not None: + set_elt.addElement('index', content=unicode(self.index)) + + if self.before is not None: + if self.before == '': # request the last page + set_elt.addElement('before') + else: + set_elt.addElement('before', content=self.before) + + if self.after is not None: + set_elt.addElement('after', content=self.after) + + return set_elt + + def render(self, element): + """Embed the DOM representation of this RSM request in the given element. + + @param element: Element to contain the RSM request. + @type element: L{domish.Element} + + @return: RSM request element. + @rtype: L{domish.Element} + """ + set_elt = self.toElement() + element.addChild(set_elt) + + return set_elt + + +class RSMResponse(object): + """ + A Result Set Management response. + + @ivar first: ID of the first element of the returned page. + @itype first: C{unicode} + + @ivar last: ID of the last element of the returned page. + @itype last: C{unicode} + + @ivar index: starting index of the returned page. + @itype index: C{int} + + @ivar count: total number of items. + @itype count: C{int} + + """ + + def __init__(self, first=None, last=None, index=None, count=None): + if first is None: + assert last is None and index is None + if last is None: + assert first is None + self.first = first + self.last = last + if count is not None: + self.count = int(count) + else: + self.count = None + if index is not None: + self.index = int(index) + else: + self.index = None + + def __str__(self): + return "RSM Request: first={0.first} last={0.last} index={0.index} count={0.count}".format(self) + + @classmethod + def fromElement(cls, element): + """Parse the given response element. + + @param element: response element. + @type element: L{domish.Element} + + @return: RSMResponse instance. + @rtype: L{RSMResponse} + """ + try: + set_elt = element.elements(NS_RSM, 'set').next() + except StopIteration: + raise RSMNotFoundError() + + try: + first_elt = set_elt.elements(NS_RSM, 'first').next() + except StopIteration: + first = None + index = None + else: + first = unicode(first_elt) + try: + index = int(first_elt['index']) + except KeyError: + index = None + except ValueError: + raise RSMError("bad index in RSM response") + + try: + last_elt = set_elt.elements(NS_RSM, 'last').next() + except StopIteration: + if first is not None: + raise RSMError("RSM response is missing its 'last' element") + else: + last = None + else: + if first is None: + raise RSMError("RSM response is missing its 'first' element") + last = unicode(last_elt) + + try: + count_elt = set_elt.elements(NS_RSM, 'count').next() + except StopIteration: + count = None + else: + try: + count = int(unicode(count_elt)) + except ValueError: + raise RSMError("invalid count in RSM response") + + return RSMResponse(first, last, index, count) + + def toElement(self): + """ + Return the DOM representation of this RSM request. + + @rtype: L{domish.Element} + """ + set_elt = domish.Element((NS_RSM, 'set')) + if self.first is not None: + first_elt = set_elt.addElement('first', content=self.first) + if self.index is not None: + first_elt['index'] = unicode(self.index) + + set_elt.addElement('last', content=self.last) + + if self.count is not None: + set_elt.addElement('count', content=unicode(self.count)) + + return set_elt + + def render(self, element): + """Embed the DOM representation of this RSM response in the given element. + + @param element: Element to contain the RSM response. + @type element: L{domish.Element} + + @return: RSM request element. + @rtype: L{domish.Element} + """ + set_elt = self.toElement() + element.addChild(set_elt) + return set_elt + + def toDict(self): + """Return a dict representation of the object. + + @return: a dict of strings. + @rtype: C{dict} binding C{unicode} to C{unicode} + """ + result = {} + for attr in ('first', 'last', 'index', 'count'): + value = getattr(self, attr) + if value is not None: + result[attr] = unicode(value) + return result + + +class PubSubRequest(pubsub.PubSubRequest): + """PubSubRequest extension to handle RSM. + + @ivar rsm: RSM request instance. + @type rsm: L{RSMRequest} + """ + + rsm = None + _parameters = copy.deepcopy(pubsub.PubSubRequest._parameters) + _parameters['items'].append('rsm') + + def _parse_rsm(self, verbElement): + try: + self.rsm = RSMRequest.fromElement(verbElement.parent) + except RSMNotFoundError: + self.rsm = None + + def _render_rsm(self, verbElement): + if self.rsm: + self.rsm.render(verbElement.parent) + + +class PubSubClient(pubsub.PubSubClient): + """PubSubClient extension to handle RSM.""" + + _request_class = PubSubRequest + + def items(self, service, nodeIdentifier, maxItems=None, itemIdentifiers=None, + subscriptionIdentifier=None, sender=None, rsm_request=None): + """ + Retrieve previously published items from a publish subscribe node. + + @param service: The publish subscribe service that keeps the node. + @type service: L{JID} + + @param nodeIdentifier: The identifier of the node. + @type nodeIdentifier: C{unicode} + + @param maxItems: Optional limit on the number of retrieved items. + @type maxItems: C{int} + + @param itemIdentifiers: Identifiers of the items to be retrieved. + @type itemIdentifiers: C{set} + + @param subscriptionIdentifier: Optional subscription identifier. In + case the node has been subscribed to multiple times, this narrows + the results to the specific subscription. + @type subscriptionIdentifier: C{unicode} + + @param ext_data: extension data. + @type ext_data: L{dict} + + @return: a Deferred that fires a C{list} of C{tuple} of L{domish.Element}, L{RSMResponse}. + @rtype: L{defer.Deferred} + """ + # XXX: we have to copy initial method instead of calling it, + # as original cb remove all non item elements + request = self._request_class('items') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + if maxItems: + request.maxItems = str(int(maxItems)) + request.subscriptionIdentifier = subscriptionIdentifier + request.sender = sender + request.itemIdentifiers = itemIdentifiers + request.rsm = rsm_request + + def cb(iq): + items = [] + pubsub_elt = iq.pubsub + if pubsub_elt.items: + for element in pubsub_elt.items.elements(pubsub.NS_PUBSUB, 'item'): + items.append(element) + + try: + rsm_response = RSMResponse.fromElement(pubsub_elt) + except RSMNotFoundError: + rsm_response = None + return (items, rsm_response) + + d = request.send(self.xmlstream) + d.addCallback(cb) + return d + + +class PubSubService(pubsub.PubSubService): + """PubSubService extension to handle RSM.""" + + _request_class = PubSubRequest + + def _toResponse_items(self, elts, resource, request): + # default method only manage elements + # but we need to add RSM set element + rsm_elt = None + for idx, elt in enumerate(reversed(elts)): + if elt.name == "set" and elt.uri == NS_RSM: + rsm_elt = elts.pop(-1-idx) + break + + response = pubsub.PubSubService._toResponse_items(self, elts, + resource, request) + if rsm_elt is not None: + response.addChild(rsm_elt) + + return response diff -r 7430d1f6db22 -r c8cb4e867897 sat_tmp/wokkel/test/__init__.py diff -r 7430d1f6db22 -r c8cb4e867897 sat_tmp/wokkel/test/test_pubsub.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat_tmp/wokkel/test/test_pubsub.py Thu Nov 02 22:50:59 2017 +0100 @@ -0,0 +1,4218 @@ +# Copyright (c) Ralph Meijer. +# See LICENSE for details. + +""" +Tests for L{wokkel.pubsub} +""" + +from zope.interface import verify + +from twisted.trial import unittest +from twisted.internet import defer +from twisted.words.xish import domish +from twisted.words.protocols.jabber import error +from twisted.words.protocols.jabber.jid import JID +from twisted.words.protocols.jabber.xmlstream import toResponse + +from wokkel import data_form, disco, iwokkel, shim +from wokkel.generic import parseXml +from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub + +from sat.tmp.wokkel import pubsub + +NS_PUBSUB = 'http://jabber.org/protocol/pubsub' +NS_PUBSUB_NODE_CONFIG = 'http://jabber.org/protocol/pubsub#node_config' +NS_PUBSUB_ERRORS = 'http://jabber.org/protocol/pubsub#errors' +NS_PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event' +NS_PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner' +NS_PUBSUB_META_DATA = 'http://jabber.org/protocol/pubsub#meta-data' +NS_PUBSUB_SUBSCRIBE_OPTIONS = 'http://jabber.org/protocol/pubsub#subscribe_options' + +def calledAsync(fn): + """ + Function wrapper that fires a deferred upon calling the given function. + """ + d = defer.Deferred() + + def func(*args, **kwargs): + try: + result = fn(*args, **kwargs) + except: + d.errback() + else: + d.callback(result) + + return d, func + + +class SubscriptionTest(unittest.TestCase): + """ + Tests for L{pubsub.Subscription}. + """ + + def test_fromElement(self): + """ + fromElement parses a subscription from XML DOM. + """ + xml = """ + + """ + subscription = pubsub.Subscription.fromElement(parseXml(xml)) + self.assertEqual('test', subscription.nodeIdentifier) + self.assertEqual(JID('user@example.org/Home'), subscription.subscriber) + self.assertEqual('pending', subscription.state) + self.assertIdentical(None, subscription.subscriptionIdentifier) + + + def test_fromElementWithSubscriptionIdentifier(self): + """ + A subscription identifier in the subscription should be parsed, too. + """ + xml = """ + + """ + subscription = pubsub.Subscription.fromElement(parseXml(xml)) + self.assertEqual('1234', subscription.subscriptionIdentifier) + + + def test_toElement(self): + """ + Rendering a Subscription should yield the proper attributes. + """ + subscription = pubsub.Subscription('test', + JID('user@example.org/Home'), + 'pending') + element = subscription.toElement() + self.assertEqual('subscription', element.name) + self.assertEqual(None, element.uri) + self.assertEqual('test', element.getAttribute('node')) + self.assertEqual('user@example.org/Home', element.getAttribute('jid')) + self.assertEqual('pending', element.getAttribute('subscription')) + self.assertFalse(element.hasAttribute('subid')) + + + def test_toElementEmptyNodeIdentifier(self): + """ + The empty node identifier should not yield a node attribute. + """ + subscription = pubsub.Subscription('', + JID('user@example.org/Home'), + 'pending') + element = subscription.toElement() + self.assertFalse(element.hasAttribute('node')) + + + def test_toElementWithSubscriptionIdentifier(self): + """ + The subscription identifier, if set, is in the subid attribute. + """ + subscription = pubsub.Subscription('test', + JID('user@example.org/Home'), + 'pending', + subscriptionIdentifier='1234') + element = subscription.toElement() + self.assertEqual('1234', element.getAttribute('subid')) + + + +class PubSubClientTest(unittest.TestCase): + timeout = 2 + + def setUp(self): + self.stub = XmlStreamStub() + self.protocol = pubsub.PubSubClient() + self.protocol.xmlstream = self.stub.xmlstream + self.protocol.connectionInitialized() + + + def test_interface(self): + """ + Do instances of L{pubsub.PubSubClient} provide L{iwokkel.IPubSubClient}? + """ + verify.verifyObject(iwokkel.IPubSubClient, self.protocol) + + + def test_eventItems(self): + """ + Test receiving an items event resulting in a call to itemsReceived. + """ + message = domish.Element((None, 'message')) + message['from'] = 'pubsub.example.org' + message['to'] = 'user@example.org/home' + event = message.addElement((NS_PUBSUB_EVENT, 'event')) + items = event.addElement('items') + items['node'] = 'test' + item1 = items.addElement('item') + item1['id'] = 'item1' + item2 = items.addElement('retract') + item2['id'] = 'item2' + item3 = items.addElement('item') + item3['id'] = 'item3' + + def itemsReceived(event): + self.assertEquals(JID('user@example.org/home'), event.recipient) + self.assertEquals(JID('pubsub.example.org'), event.sender) + self.assertEquals('test', event.nodeIdentifier) + self.assertEquals([item1, item2, item3], event.items) + + d, self.protocol.itemsReceived = calledAsync(itemsReceived) + self.stub.send(message) + return d + + + def test_eventItemsCollection(self): + """ + Test receiving an items event resulting in a call to itemsReceived. + """ + message = domish.Element((None, 'message')) + message['from'] = 'pubsub.example.org' + message['to'] = 'user@example.org/home' + event = message.addElement((NS_PUBSUB_EVENT, 'event')) + items = event.addElement('items') + items['node'] = 'test' + + headers = shim.Headers([('Collection', 'collection')]) + message.addChild(headers) + + def itemsReceived(event): + self.assertEquals(JID('user@example.org/home'), event.recipient) + self.assertEquals(JID('pubsub.example.org'), event.sender) + self.assertEquals('test', event.nodeIdentifier) + self.assertEquals({'Collection': ['collection']}, event.headers) + + d, self.protocol.itemsReceived = calledAsync(itemsReceived) + self.stub.send(message) + return d + + + def test_eventItemsError(self): + """ + An error message with embedded event should not be handled. + + This test uses an items event, which should not result in itemsReceived + being called. In general message.handled should be False. + """ + message = domish.Element((None, 'message')) + message['from'] = 'pubsub.example.org' + message['to'] = 'user@example.org/home' + message['type'] = 'error' + event = message.addElement((NS_PUBSUB_EVENT, 'event')) + items = event.addElement('items') + items['node'] = 'test' + + class UnexpectedCall(Exception): + pass + + def itemsReceived(event): + raise UnexpectedCall("Unexpected call to itemsReceived") + + self.protocol.itemsReceived = itemsReceived + self.stub.send(message) + self.assertFalse(message.handled) + + + def test_eventDelete(self): + """ + Test receiving a delete event resulting in a call to deleteReceived. + """ + message = domish.Element((None, 'message')) + message['from'] = 'pubsub.example.org' + message['to'] = 'user@example.org/home' + event = message.addElement((NS_PUBSUB_EVENT, 'event')) + delete = event.addElement('delete') + delete['node'] = 'test' + + def deleteReceived(event): + self.assertEquals(JID('user@example.org/home'), event.recipient) + self.assertEquals(JID('pubsub.example.org'), event.sender) + self.assertEquals('test', event.nodeIdentifier) + + d, self.protocol.deleteReceived = calledAsync(deleteReceived) + self.stub.send(message) + return d + + + def test_eventDeleteRedirect(self): + """ + Test receiving a delete event with a redirect URI. + """ + message = domish.Element((None, 'message')) + message['from'] = 'pubsub.example.org' + message['to'] = 'user@example.org/home' + event = message.addElement((NS_PUBSUB_EVENT, 'event')) + delete = event.addElement('delete') + delete['node'] = 'test' + uri = 'xmpp:pubsub.example.org?;node=test2' + delete.addElement('redirect')['uri'] = uri + + def deleteReceived(event): + self.assertEquals(JID('user@example.org/home'), event.recipient) + self.assertEquals(JID('pubsub.example.org'), event.sender) + self.assertEquals('test', event.nodeIdentifier) + self.assertEquals(uri, event.redirectURI) + + d, self.protocol.deleteReceived = calledAsync(deleteReceived) + self.stub.send(message) + return d + + + def test_event_purge(self): + """ + Test receiving a purge event resulting in a call to purgeReceived. + """ + message = domish.Element((None, 'message')) + message['from'] = 'pubsub.example.org' + message['to'] = 'user@example.org/home' + event = message.addElement((NS_PUBSUB_EVENT, 'event')) + items = event.addElement('purge') + items['node'] = 'test' + + def purgeReceived(event): + self.assertEquals(JID('user@example.org/home'), event.recipient) + self.assertEquals(JID('pubsub.example.org'), event.sender) + self.assertEquals('test', event.nodeIdentifier) + + d, self.protocol.purgeReceived = calledAsync(purgeReceived) + self.stub.send(message) + return d + + + def test_createNode(self): + """ + Test sending create request. + """ + + def cb(nodeIdentifier): + self.assertEquals('test', nodeIdentifier) + + d = self.protocol.createNode(JID('pubsub.example.org'), 'test') + d.addCallback(cb) + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('set', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'create', NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + + response = toResponse(iq, 'result') + self.stub.send(response) + return d + + + def test_createNodeInstant(self): + """ + Test sending create request resulting in an instant node. + """ + + def cb(nodeIdentifier): + self.assertEquals('test', nodeIdentifier) + + d = self.protocol.createNode(JID('pubsub.example.org')) + d.addCallback(cb) + + iq = self.stub.output[-1] + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'create', NS_PUBSUB)) + child = children[0] + self.assertFalse(child.hasAttribute('node')) + + response = toResponse(iq, 'result') + command = response.addElement((NS_PUBSUB, 'pubsub')) + create = command.addElement('create') + create['node'] = 'test' + self.stub.send(response) + return d + + + def test_createNodeRenamed(self): + """ + Test sending create request resulting in renamed node. + """ + + def cb(nodeIdentifier): + self.assertEquals('test2', nodeIdentifier) + + d = self.protocol.createNode(JID('pubsub.example.org'), 'test') + d.addCallback(cb) + + iq = self.stub.output[-1] + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'create', NS_PUBSUB)) + child = children[0] + self.assertEquals('test', child['node']) + + response = toResponse(iq, 'result') + command = response.addElement((NS_PUBSUB, 'pubsub')) + create = command.addElement('create') + create['node'] = 'test2' + self.stub.send(response) + return d + + + def test_createNodeWithSender(self): + """ + Test sending create request from a specific JID. + """ + + d = self.protocol.createNode(JID('pubsub.example.org'), 'test', + sender=JID('user@example.org')) + + iq = self.stub.output[-1] + self.assertEquals('user@example.org', iq['from']) + + response = toResponse(iq, 'result') + self.stub.send(response) + return d + + + def test_createNodeWithConfig(self): + """ + Test sending create request with configuration options + """ + + options = { + 'pubsub#title': 'Princely Musings (Atom)', + 'pubsub#deliver_payloads': True, + 'pubsub#persist_items': '1', + 'pubsub#max_items': '10', + 'pubsub#access_model': 'open', + 'pubsub#type': 'http://www.w3.org/2005/Atom', + } + + d = self.protocol.createNode(JID('pubsub.example.org'), 'test', + sender=JID('user@example.org'), + options=options) + + iq = self.stub.output[-1] + + # check if there is exactly one configure element + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'configure', NS_PUBSUB)) + self.assertEqual(1, len(children)) + + # check that it has a configuration form + form = data_form.findForm(children[0], NS_PUBSUB_NODE_CONFIG) + self.assertEqual('submit', form.formType) + + + response = toResponse(iq, 'result') + self.stub.send(response) + return d + + + def test_deleteNode(self): + """ + Test sending delete request. + """ + + d = self.protocol.deleteNode(JID('pubsub.example.org'), 'test') + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('set', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(NS_PUBSUB_OWNER, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'delete', NS_PUBSUB_OWNER)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + + response = toResponse(iq, 'result') + self.stub.send(response) + return d + + + def test_deleteNodeWithSender(self): + """ + Test sending delete request. + """ + + d = self.protocol.deleteNode(JID('pubsub.example.org'), 'test', + sender=JID('user@example.org')) + + iq = self.stub.output[-1] + self.assertEquals('user@example.org', iq['from']) + + response = toResponse(iq, 'result') + self.stub.send(response) + return d + + + def test_publish(self): + """ + Test sending publish request. + """ + + item = pubsub.Item() + d = self.protocol.publish(JID('pubsub.example.org'), 'test', [item]) + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('set', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'publish', NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + items = list(domish.generateElementsQNamed(child.children, + 'item', NS_PUBSUB)) + self.assertEquals(1, len(items)) + self.assertIdentical(item, items[0]) + + response = toResponse(iq, 'result') + self.stub.send(response) + return d + + + def test_publishNoItems(self): + """ + Test sending publish request without items. + """ + + d = self.protocol.publish(JID('pubsub.example.org'), 'test') + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('set', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'publish', NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + + response = toResponse(iq, 'result') + self.stub.send(response) + return d + + + def test_publishWithSender(self): + """ + Test sending publish request from a specific JID. + """ + + item = pubsub.Item() + d = self.protocol.publish(JID('pubsub.example.org'), 'test', [item], + JID('user@example.org')) + + iq = self.stub.output[-1] + self.assertEquals('user@example.org', iq['from']) + + response = toResponse(iq, 'result') + self.stub.send(response) + return d + + + def test_subscribe(self): + """ + Test sending subscription request. + """ + d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', + JID('user@example.org')) + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('set', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'subscribe', NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + self.assertEquals('user@example.org', child['jid']) + + response = toResponse(iq, 'result') + pubsub = response.addElement((NS_PUBSUB, 'pubsub')) + subscription = pubsub.addElement('subscription') + subscription['node'] = 'test' + subscription['jid'] = 'user@example.org' + subscription['subscription'] = 'subscribed' + self.stub.send(response) + return d + + + def test_subscribeReturnsSubscription(self): + """ + A successful subscription should return a Subscription instance. + """ + def cb(subscription): + self.assertEqual(JID('user@example.org'), subscription.subscriber) + + d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', + JID('user@example.org')) + d.addCallback(cb) + + iq = self.stub.output[-1] + + response = toResponse(iq, 'result') + pubsub = response.addElement((NS_PUBSUB, 'pubsub')) + subscription = pubsub.addElement('subscription') + subscription['node'] = 'test' + subscription['jid'] = 'user@example.org' + subscription['subscription'] = 'subscribed' + self.stub.send(response) + return d + + + def test_subscribePending(self): + """ + Test sending subscription request that results in a pending + subscription. + """ + d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', + JID('user@example.org')) + + iq = self.stub.output[-1] + response = toResponse(iq, 'result') + command = response.addElement((NS_PUBSUB, 'pubsub')) + subscription = command.addElement('subscription') + subscription['node'] = 'test' + subscription['jid'] = 'user@example.org' + subscription['subscription'] = 'pending' + self.stub.send(response) + self.assertFailure(d, pubsub.SubscriptionPending) + return d + + + def test_subscribeUnconfigured(self): + """ + Test sending subscription request that results in an unconfigured + subscription. + """ + d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', + JID('user@example.org')) + + iq = self.stub.output[-1] + response = toResponse(iq, 'result') + command = response.addElement((NS_PUBSUB, 'pubsub')) + subscription = command.addElement('subscription') + subscription['node'] = 'test' + subscription['jid'] = 'user@example.org' + subscription['subscription'] = 'unconfigured' + self.stub.send(response) + self.assertFailure(d, pubsub.SubscriptionUnconfigured) + return d + + + def test_subscribeWithOptions(self): + options = {'pubsub#deliver': False} + + d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', + JID('user@example.org'), + options=options) + iq = self.stub.output[-1] + + # Check options present + childNames = [] + for element in iq.pubsub.elements(): + if element.uri == NS_PUBSUB: + childNames.append(element.name) + + self.assertEqual(['subscribe', 'options'], childNames) + form = data_form.findForm(iq.pubsub.options, + NS_PUBSUB_SUBSCRIBE_OPTIONS) + self.assertEqual('submit', form.formType) + form.typeCheck({'pubsub#deliver': {'type': 'boolean'}}) + self.assertEqual(options, form.getValues()) + + # Send response + response = toResponse(iq, 'result') + pubsub = response.addElement((NS_PUBSUB, 'pubsub')) + subscription = pubsub.addElement('subscription') + subscription['node'] = 'test' + subscription['jid'] = 'user@example.org' + subscription['subscription'] = 'subscribed' + self.stub.send(response) + + return d + + + def test_subscribeWithSender(self): + """ + Test sending subscription request from a specific JID. + """ + d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', + JID('user@example.org'), + sender=JID('user@example.org')) + + iq = self.stub.output[-1] + self.assertEquals('user@example.org', iq['from']) + + response = toResponse(iq, 'result') + pubsub = response.addElement((NS_PUBSUB, 'pubsub')) + subscription = pubsub.addElement('subscription') + subscription['node'] = 'test' + subscription['jid'] = 'user@example.org' + subscription['subscription'] = 'subscribed' + self.stub.send(response) + return d + + + def test_subscribeReturningSubscriptionIdentifier(self): + """ + Test sending subscription request with subscription identifier. + """ + def cb(subscription): + self.assertEqual('1234', subscription.subscriptionIdentifier) + + d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', + JID('user@example.org')) + d.addCallback(cb) + + iq = self.stub.output[-1] + + response = toResponse(iq, 'result') + pubsub = response.addElement((NS_PUBSUB, 'pubsub')) + subscription = pubsub.addElement('subscription') + subscription['node'] = 'test' + subscription['jid'] = 'user@example.org' + subscription['subscription'] = 'subscribed' + subscription['subid'] = '1234' + self.stub.send(response) + return d + + + def test_unsubscribe(self): + """ + Test sending unsubscription request. + """ + d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test', + JID('user@example.org')) + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('set', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'unsubscribe', NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + self.assertEquals('user@example.org', child['jid']) + + self.stub.send(toResponse(iq, 'result')) + return d + + + def test_unsubscribeWithSender(self): + """ + Test sending unsubscription request from a specific JID. + """ + d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test', + JID('user@example.org'), + sender=JID('user@example.org')) + + iq = self.stub.output[-1] + self.assertEquals('user@example.org', iq['from']) + self.stub.send(toResponse(iq, 'result')) + return d + + + def test_unsubscribeWithSubscriptionIdentifier(self): + """ + Test sending unsubscription request with subscription identifier. + """ + d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test', + JID('user@example.org'), + subscriptionIdentifier='1234') + + iq = self.stub.output[-1] + child = iq.pubsub.unsubscribe + self.assertEquals('1234', child['subid']) + + self.stub.send(toResponse(iq, 'result')) + return d + + + def test_items(self): + """ + Test sending items request. + """ + def cb(items): + self.assertEquals([], items) + + d = self.protocol.items(JID('pubsub.example.org'), 'test') + d.addCallback(cb) + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('get', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'items', NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + + response = toResponse(iq, 'result') + items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') + items['node'] = 'test' + + self.stub.send(response) + + return d + + + def test_itemsMaxItems(self): + """ + Test sending items request, with limit on the number of items. + """ + def cb(items): + self.assertEquals(2, len(items)) + self.assertEquals([item1, item2], items) + + d = self.protocol.items(JID('pubsub.example.org'), 'test', maxItems=2) + d.addCallback(cb) + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('get', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'items', NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + self.assertEquals('2', child['max_items']) + + response = toResponse(iq, 'result') + items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') + items['node'] = 'test' + item1 = items.addElement('item') + item1['id'] = 'item1' + item2 = items.addElement('item') + item2['id'] = 'item2' + + self.stub.send(response) + + return d + + + def test_itemsWithItemIdentifiers(self): + """ + Test sending items request with item identifiers. + """ + def cb(items): + self.assertEquals(2, len(items)) + self.assertEquals([item1, item2], items) + + d = self.protocol.items(JID('pubsub.example.org'), 'test', + itemIdentifiers=['item1', 'item2']) + d.addCallback(cb) + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('get', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'items', NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + itemIdentifiers = [item.getAttribute('id') for item in + domish.generateElementsQNamed(child.children, 'item', + NS_PUBSUB)] + self.assertEquals(['item1', 'item2'], itemIdentifiers) + + response = toResponse(iq, 'result') + items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') + items['node'] = 'test' + item1 = items.addElement('item') + item1['id'] = 'item1' + item2 = items.addElement('item') + item2['id'] = 'item2' + + self.stub.send(response) + + return d + + + def test_itemsWithSubscriptionIdentifier(self): + """ + Test sending items request with a subscription identifier. + """ + + d = self.protocol.items(JID('pubsub.example.org'), 'test', + subscriptionIdentifier='1234') + + iq = self.stub.output[-1] + child = iq.pubsub.items + self.assertEquals('1234', child['subid']) + + response = toResponse(iq, 'result') + items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') + items['node'] = 'test' + + self.stub.send(response) + return d + + + def test_itemsWithSender(self): + """ + Test sending items request from a specific JID. + """ + + d = self.protocol.items(JID('pubsub.example.org'), 'test', + sender=JID('user@example.org')) + + iq = self.stub.output[-1] + self.assertEquals('user@example.org', iq['from']) + + response = toResponse(iq, 'result') + items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') + items['node'] = 'test' + + self.stub.send(response) + return d + + + def test_retractItems(self): + """ + Test sending items retraction. + """ + d = self.protocol.retractItems(JID('pubsub.example.org'), 'test', + itemIdentifiers=['item1', 'item2']) + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('set', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'retract', NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + itemIdentifiers = [item.getAttribute('id') for item in + domish.generateElementsQNamed(child.children, 'item', + NS_PUBSUB)] + self.assertEquals(['item1', 'item2'], itemIdentifiers) + + self.stub.send(toResponse(iq, 'result')) + return d + + + def test_retractItemsWithSender(self): + """ + Test retracting items request from a specific JID. + """ + d = self.protocol.retractItems(JID('pubsub.example.org'), 'test', + itemIdentifiers=['item1', 'item2'], + sender=JID('user@example.org')) + + iq = self.stub.output[-1] + self.assertEquals('user@example.org', iq['from']) + + self.stub.send(toResponse(iq, 'result')) + return d + + + def test_getOptions(self): + def cb(form): + self.assertEqual('form', form.formType) + self.assertEqual(NS_PUBSUB_SUBSCRIBE_OPTIONS, form.formNamespace) + field = form.fields['pubsub#deliver'] + self.assertEqual('boolean', field.fieldType) + self.assertIdentical(True, field.value) + self.assertEqual('Enable delivery?', field.label) + + d = self.protocol.getOptions(JID('pubsub.example.org'), 'test', + JID('user@example.org'), + sender=JID('user@example.org')) + d.addCallback(cb) + + iq = self.stub.output[-1] + self.assertEqual('pubsub.example.org', iq.getAttribute('to')) + self.assertEqual('get', iq.getAttribute('type')) + self.assertEqual('pubsub', iq.pubsub.name) + self.assertEqual(NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'options', NS_PUBSUB)) + self.assertEqual(1, len(children)) + child = children[0] + self.assertEqual('test', child['node']) + + self.assertEqual(0, len(child.children)) + + # Send response + form = data_form.Form('form', formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) + form.addField(data_form.Field('boolean', var='pubsub#deliver', + label='Enable delivery?', + value=True)) + response = toResponse(iq, 'result') + response.addElement((NS_PUBSUB, 'pubsub')) + response.pubsub.addElement('options') + response.pubsub.options.addChild(form.toElement()) + self.stub.send(response) + + return d + + + def test_getOptionsWithSubscriptionIdentifier(self): + """ + Getting options with a subid should have the subid in the request. + """ + + d = self.protocol.getOptions(JID('pubsub.example.org'), 'test', + JID('user@example.org'), + sender=JID('user@example.org'), + subscriptionIdentifier='1234') + + iq = self.stub.output[-1] + child = iq.pubsub.options + self.assertEqual('1234', child['subid']) + + # Send response + form = data_form.Form('form', formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) + form.addField(data_form.Field('boolean', var='pubsub#deliver', + label='Enable delivery?', + value=True)) + response = toResponse(iq, 'result') + response.addElement((NS_PUBSUB, 'pubsub')) + response.pubsub.addElement('options') + response.pubsub.options.addChild(form.toElement()) + self.stub.send(response) + + return d + + + def test_setOptions(self): + """ + setOptions should send out a options-set request. + """ + options = {'pubsub#deliver': False} + + d = self.protocol.setOptions(JID('pubsub.example.org'), 'test', + JID('user@example.org'), + options, + sender=JID('user@example.org')) + + iq = self.stub.output[-1] + self.assertEqual('pubsub.example.org', iq.getAttribute('to')) + self.assertEqual('set', iq.getAttribute('type')) + self.assertEqual('pubsub', iq.pubsub.name) + self.assertEqual(NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'options', NS_PUBSUB)) + self.assertEqual(1, len(children)) + child = children[0] + self.assertEqual('test', child['node']) + + form = data_form.findForm(child, NS_PUBSUB_SUBSCRIBE_OPTIONS) + self.assertEqual('submit', form.formType) + form.typeCheck({'pubsub#deliver': {'type': 'boolean'}}) + self.assertEqual(options, form.getValues()) + + response = toResponse(iq, 'result') + self.stub.send(response) + + return d + + + def test_setOptionsWithSubscriptionIdentifier(self): + """ + setOptions should send out a options-set request with subid. + """ + options = {'pubsub#deliver': False} + + d = self.protocol.setOptions(JID('pubsub.example.org'), 'test', + JID('user@example.org'), + options, + subscriptionIdentifier='1234', + sender=JID('user@example.org')) + + iq = self.stub.output[-1] + child = iq.pubsub.options + self.assertEqual('1234', child['subid']) + + form = data_form.findForm(child, NS_PUBSUB_SUBSCRIBE_OPTIONS) + self.assertEqual('submit', form.formType) + form.typeCheck({'pubsub#deliver': {'type': 'boolean'}}) + self.assertEqual(options, form.getValues()) + + response = toResponse(iq, 'result') + self.stub.send(response) + + return d + + +class PubSubRequestTest(unittest.TestCase): + + def test_fromElementUnknown(self): + """ + An unknown verb raises NotImplementedError. + """ + + xml = """ + + + + + + """ + + self.assertRaises(NotImplementedError, + pubsub.PubSubRequest.fromElement, parseXml(xml)) + + + def test_fromElementKnownBadCombination(self): + """ + Multiple verbs in an unknown configuration raises NotImplementedError. + """ + + xml = """ + + + + + + + """ + + self.assertRaises(NotImplementedError, + pubsub.PubSubRequest.fromElement, parseXml(xml)) + + def test_fromElementPublish(self): + """ + Test parsing a publish request. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('publish', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + self.assertEqual('test', request.nodeIdentifier) + self.assertEqual([], request.items) + + + def test_fromElementPublishItems(self): + """ + Test parsing a publish request with items. + """ + + xml = """ + + + + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual(2, len(request.items)) + self.assertEqual(u'item1', request.items[0]["id"]) + self.assertEqual(u'item2', request.items[1]["id"]) + + + def test_fromElementPublishItemsOptions(self): + """ + Test parsing a publish request with items and options. + + Note that publishing options are not supported, but passing them + shouldn't affect processing of the publish request itself. + """ + + xml = """ + + + + + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual(2, len(request.items)) + self.assertEqual(u'item1', request.items[0]["id"]) + self.assertEqual(u'item2', request.items[1]["id"]) + + def test_fromElementPublishNoNode(self): + """ + A publish request to the root node should raise an exception. + """ + xml = """ + + + + + + """ + + err = self.assertRaises(error.StanzaError, + pubsub.PubSubRequest.fromElement, + parseXml(xml)) + self.assertEqual('bad-request', err.condition) + self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri) + self.assertEqual('nodeid-required', err.appCondition.name) + + + def test_fromElementSubscribe(self): + """ + Test parsing a subscription request. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('subscribe', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + self.assertEqual('test', request.nodeIdentifier) + self.assertEqual(JID('user@example.org/Home'), request.subscriber) + + + def test_fromElementSubscribeEmptyNode(self): + """ + Test parsing a subscription request to the root node. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('', request.nodeIdentifier) + + + def test_fromElementSubscribeNoJID(self): + """ + Subscribe requests without a JID should raise a bad-request exception. + """ + xml = """ + + + + + + """ + err = self.assertRaises(error.StanzaError, + pubsub.PubSubRequest.fromElement, + parseXml(xml)) + self.assertEqual('bad-request', err.condition) + self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri) + self.assertEqual('jid-required', err.appCondition.name) + + + def test_fromElementSubscribeWithOptions(self): + """ + Test parsing a subscription request. + """ + + xml = """ + + + + + + + http://jabber.org/protocol/pubsub#subscribe_options + + + 1 + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('subscribe', request.verb) + request.options.typeCheck({'pubsub#deliver': {'type': 'boolean'}}) + self.assertEqual({'pubsub#deliver': True}, request.options.getValues()) + + + def test_fromElementSubscribeWithOptionsBadFormType(self): + """ + The options form should have the right type. + """ + + xml = """ + + + + + + + http://jabber.org/protocol/pubsub#subscribe_options + + + 1 + + + + + + """ + + err = self.assertRaises(error.StanzaError, + pubsub.PubSubRequest.fromElement, + parseXml(xml)) + self.assertEqual('bad-request', err.condition) + self.assertEqual("Unexpected form type 'result'", err.text) + self.assertEqual(None, err.appCondition) + + + def test_fromElementSubscribeWithOptionsEmpty(self): + """ + When no (suitable) form is found, the options are empty. + """ + + xml = """ + + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('subscribe', request.verb) + self.assertEqual({}, request.options.getValues()) + + + def test_fromElementUnsubscribe(self): + """ + Test parsing an unsubscription request. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('unsubscribe', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + self.assertEqual('test', request.nodeIdentifier) + self.assertEqual(JID('user@example.org/Home'), request.subscriber) + + + def test_fromElementUnsubscribeWithSubscriptionIdentifier(self): + """ + Test parsing an unsubscription request with subscription identifier. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('1234', request.subscriptionIdentifier) + + + def test_fromElementUnsubscribeNoJID(self): + """ + Unsubscribe requests without a JID should raise a bad-request exception. + """ + xml = """ + + + + + + """ + err = self.assertRaises(error.StanzaError, + pubsub.PubSubRequest.fromElement, + parseXml(xml)) + self.assertEqual('bad-request', err.condition) + self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri) + self.assertEqual('jid-required', err.appCondition.name) + + + def test_fromElementOptionsGet(self): + """ + Test parsing a request for getting subscription options. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('optionsGet', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + self.assertEqual('test', request.nodeIdentifier) + self.assertEqual(JID('user@example.org/Home'), request.subscriber) + + + def test_fromElementOptionsGetWithSubscriptionIdentifier(self): + """ + Test parsing a request for getting subscription options with subid. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('1234', request.subscriptionIdentifier) + + + def test_fromElementOptionsSet(self): + """ + Test parsing a request for setting subscription options. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#subscribe_options + + 1 + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('optionsSet', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + self.assertEqual('test', request.nodeIdentifier) + self.assertEqual(JID('user@example.org/Home'), request.subscriber) + self.assertEqual({'pubsub#deliver': '1'}, request.options.getValues()) + + + def test_fromElementOptionsSetWithSubscriptionIdentifier(self): + """ + Test parsing a request for setting subscription options with subid. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#subscribe_options + + 1 + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('1234', request.subscriptionIdentifier) + + + def test_fromElementOptionsSetCancel(self): + """ + Test parsing a request for cancelling setting subscription options. + """ + + xml = """ + + + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('cancel', request.options.formType) + + + def test_fromElementOptionsSetBadFormType(self): + """ + On a options set request unknown fields should be ignored. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#subscribe_options + + 1 + + + + + """ + + err = self.assertRaises(error.StanzaError, + pubsub.PubSubRequest.fromElement, + parseXml(xml)) + self.assertEqual('bad-request', err.condition) + self.assertEqual("Unexpected form type 'result'", err.text) + self.assertEqual(None, err.appCondition) + + + def test_fromElementOptionsSetNoForm(self): + """ + On a options set request a form is required. + """ + + xml = """ + + + + + + """ + err = self.assertRaises(error.StanzaError, + pubsub.PubSubRequest.fromElement, + parseXml(xml)) + self.assertEqual('bad-request', err.condition) + self.assertEqual(None, err.appCondition) + + + def test_fromElementSubscriptions(self): + """ + Test parsing a request for all subscriptions. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('subscriptions', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + + + def test_fromElementAffiliations(self): + """ + Test parsing a request for all affiliations. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('affiliations', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + + + def test_fromElementCreate(self): + """ + Test parsing a request to create a node. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('create', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + self.assertEqual('mynode', request.nodeIdentifier) + self.assertIdentical(None, request.options) + + + def test_fromElementCreateInstant(self): + """ + Test parsing a request to create an instant node. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertIdentical(None, request.nodeIdentifier) + + + def test_fromElementCreateConfigureEmpty(self): + """ + Test parsing a request to create a node with an empty configuration. + """ + + xml = """ + + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual({}, request.options.getValues()) + self.assertEqual(u'mynode', request.nodeIdentifier) + + + def test_fromElementCreateConfigureEmptyWrongOrder(self): + """ + Test parsing a request to create a node and configure, wrong order. + + The C{configure} element should come after the C{create} request, + but we should accept both orders. + """ + + xml = """ + + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual({}, request.options.getValues()) + self.assertEqual(u'mynode', request.nodeIdentifier) + + + def test_fromElementCreateConfigure(self): + """ + Test parsing a request to create a node. + """ + + xml = """ + + + + + + + http://jabber.org/protocol/pubsub#node_config + + open + 0 + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + values = request.options + self.assertIn('pubsub#access_model', values) + self.assertEqual(u'open', values['pubsub#access_model']) + self.assertIn('pubsub#persist_items', values) + self.assertEqual(u'0', values['pubsub#persist_items']) + + + def test_fromElementCreateConfigureBadFormType(self): + """ + The form of a node creation request should have the right type. + """ + + xml = """ + + + + + + + http://jabber.org/protocol/pubsub#node_config + + open + 0 + + + + + """ + + err = self.assertRaises(error.StanzaError, + pubsub.PubSubRequest.fromElement, + parseXml(xml)) + self.assertEqual('bad-request', err.condition) + self.assertEqual("Unexpected form type 'result'", err.text) + self.assertEqual(None, err.appCondition) + + + def test_fromElementDefault(self): + """ + Parsing default node configuration request sets required attributes. + + Besides C{verb}, C{sender} and C{recipient}, we expect C{nodeType} + to be set. If not passed it receives the default C{u'leaf'}. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEquals(u'default', request.verb) + self.assertEquals(JID('user@example.org'), request.sender) + self.assertEquals(JID('pubsub.example.org'), request.recipient) + self.assertEquals(u'leaf', request.nodeType) + + + def test_fromElementDefaultCollection(self): + """ + Parsing default request for collection sets nodeType to collection. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + + collection + + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEquals('collection', request.nodeType) + + + def test_fromElementConfigureGet(self): + """ + Test parsing a node configuration get request. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('configureGet', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + self.assertEqual('test', request.nodeIdentifier) + + + def test_fromElementConfigureSet(self): + """ + On a node configuration set request the Data Form is parsed. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + 0 + 1 + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('configureSet', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + self.assertEqual('test', request.nodeIdentifier) + self.assertEqual({'pubsub#deliver_payloads': '0', + 'pubsub#persist_items': '1'}, + request.options.getValues()) + + + def test_fromElementConfigureSetCancel(self): + """ + The node configuration is cancelled, so no options. + """ + + xml = """ + + + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('cancel', request.options.formType) + + + def test_fromElementConfigureSetBadFormType(self): + """ + The form of a node configuraton set request should have the right type. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + 0 + 1 + + + + + """ + + err = self.assertRaises(error.StanzaError, + pubsub.PubSubRequest.fromElement, + parseXml(xml)) + self.assertEqual('bad-request', err.condition) + self.assertEqual("Unexpected form type 'result'", err.text) + self.assertEqual(None, err.appCondition) + + + def test_fromElementConfigureSetNoForm(self): + """ + On a node configuration set request a form is required. + """ + + xml = """ + + + + + + """ + err = self.assertRaises(error.StanzaError, + pubsub.PubSubRequest.fromElement, + parseXml(xml)) + self.assertEqual('bad-request', err.condition) + self.assertEqual(None, err.appCondition) + + + def test_fromElementItems(self): + """ + Test parsing an items request. + """ + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('items', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + self.assertEqual('test', request.nodeIdentifier) + self.assertIdentical(None, request.maxItems) + self.assertIdentical(None, request.subscriptionIdentifier) + self.assertEqual([], request.itemIdentifiers) + + + def test_fromElementItemsSubscriptionIdentifier(self): + """ + Test parsing an items request with subscription identifier. + """ + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('1234', request.subscriptionIdentifier) + + + def test_fromElementRetract(self): + """ + Test parsing a retract request. + """ + + xml = """ + + + + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('retract', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + self.assertEqual('test', request.nodeIdentifier) + self.assertEqual(['item1', 'item2'], request.itemIdentifiers) + + + def test_fromElementPurge(self): + """ + Test parsing a purge request. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('purge', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + self.assertEqual('test', request.nodeIdentifier) + + + def test_fromElementDelete(self): + """ + Test parsing a delete request. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('delete', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + self.assertEqual('test', request.nodeIdentifier) + + + +class PubSubServiceTest(unittest.TestCase, TestableRequestHandlerMixin): + """ + Tests for L{pubsub.PubSubService}. + """ + + def setUp(self): + self.stub = XmlStreamStub() + self.resource = pubsub.PubSubResource() + self.service = pubsub.PubSubService(self.resource) + self.service.send = self.stub.xmlstream.send + + def test_interface(self): + """ + Do instances of L{pubsub.PubSubService} provide L{iwokkel.IPubSubService}? + """ + verify.verifyObject(iwokkel.IPubSubService, self.service) + + + def test_interfaceIDisco(self): + """ + Do instances of L{pubsub.PubSubService} provide L{iwokkel.IDisco}? + """ + verify.verifyObject(iwokkel.IDisco, self.service) + + + def test_connectionMade(self): + """ + Verify setup of observers in L{pubsub.connectionMade}. + """ + requests = [] + + def handleRequest(iq): + requests.append(iq) + + self.service.xmlstream = self.stub.xmlstream + self.service.handleRequest = handleRequest + self.service.connectionMade() + + for namespace in (NS_PUBSUB, NS_PUBSUB_OWNER): + for stanzaType in ('get', 'set'): + iq = domish.Element((None, 'iq')) + iq['type'] = stanzaType + iq.addElement((namespace, 'pubsub')) + self.stub.xmlstream.dispatch(iq) + + self.assertEqual(4, len(requests)) + + + def test_getDiscoInfo(self): + """ + Test getDiscoInfo calls getNodeInfo and returns some minimal info. + """ + def cb(info): + discoInfo = disco.DiscoInfo() + for item in info: + discoInfo.append(item) + self.assertIn(('pubsub', 'service'), discoInfo.identities) + self.assertIn(disco.NS_DISCO_ITEMS, discoInfo.features) + + d = self.service.getDiscoInfo(JID('user@example.org/home'), + JID('pubsub.example.org'), '') + d.addCallback(cb) + return d + + + def test_getDiscoInfoNodeType(self): + """ + Test getDiscoInfo with node type. + """ + def cb(info): + discoInfo = disco.DiscoInfo() + for item in info: + discoInfo.append(item) + self.assertIn(('pubsub', 'collection'), discoInfo.identities) + + def getInfo(requestor, target, nodeIdentifier): + return defer.succeed({'type': 'collection', + 'meta-data': {}}) + + self.resource.getInfo = getInfo + d = self.service.getDiscoInfo(JID('user@example.org/home'), + JID('pubsub.example.org'), '') + d.addCallback(cb) + return d + + + def test_getDiscoInfoMetaData(self): + """ + Test getDiscoInfo with returned meta data. + """ + def cb(info): + discoInfo = disco.DiscoInfo() + for item in info: + discoInfo.append(item) + + self.assertIn(('pubsub', 'leaf'), discoInfo.identities) + self.assertIn(NS_PUBSUB_META_DATA, discoInfo.extensions) + form = discoInfo.extensions[NS_PUBSUB_META_DATA] + self.assertIn('pubsub#node_type', form.fields) + + def getInfo(requestor, target, nodeIdentifier): + metaData = [{'var': 'pubsub#persist_items', + 'label': 'Persist items to storage', + 'value': True}] + return defer.succeed({'type': 'leaf', 'meta-data': metaData}) + + self.resource.getInfo = getInfo + d = self.service.getDiscoInfo(JID('user@example.org/home'), + JID('pubsub.example.org'), '') + d.addCallback(cb) + return d + + + def test_getDiscoInfoResourceFeatures(self): + """ + Test getDiscoInfo with the resource features. + """ + def cb(info): + discoInfo = disco.DiscoInfo() + for item in info: + discoInfo.append(item) + self.assertIn('http://jabber.org/protocol/pubsub#publish', + discoInfo.features) + + self.resource.features = ['publish'] + d = self.service.getDiscoInfo(JID('user@example.org/home'), + JID('pubsub.example.org'), '') + d.addCallback(cb) + return d + + + def test_getDiscoInfoBadResponse(self): + """ + If getInfo returns invalid response, it should be logged, then ignored. + """ + def cb(info): + self.assertEquals([], info) + self.assertEqual(1, len(self.flushLoggedErrors(TypeError))) + + def getInfo(requestor, target, nodeIdentifier): + return defer.succeed('bad response') + + self.resource.getInfo = getInfo + d = self.service.getDiscoInfo(JID('user@example.org/home'), + JID('pubsub.example.org'), 'test') + d.addCallback(cb) + return d + + + def test_getDiscoInfoException(self): + """ + If getInfo returns invalid response, it should be logged, then ignored. + """ + def cb(info): + self.assertEquals([], info) + self.assertEqual(1, len(self.flushLoggedErrors(NotImplementedError))) + + def getInfo(requestor, target, nodeIdentifier): + return defer.fail(NotImplementedError()) + + self.resource.getInfo = getInfo + d = self.service.getDiscoInfo(JID('user@example.org/home'), + JID('pubsub.example.org'), 'test') + d.addCallback(cb) + return d + + + def test_getDiscoItemsRoot(self): + """ + Test getDiscoItems on the root node. + """ + def getNodes(requestor, service, nodeIdentifier): + return defer.succeed(['node1', 'node2']) + + def cb(items): + self.assertEqual(2, len(items)) + item1, item2 = items + + self.assertEqual(JID('pubsub.example.org'), item1.entity) + self.assertEqual('node1', item1.nodeIdentifier) + + self.assertEqual(JID('pubsub.example.org'), item2.entity) + self.assertEqual('node2', item2.nodeIdentifier) + + self.resource.getNodes = getNodes + d = self.service.getDiscoItems(JID('user@example.org/home'), + JID('pubsub.example.org'), + '') + d.addCallback(cb) + return d + + + def test_getDiscoItemsRootHideNodes(self): + """ + Test getDiscoItems on the root node. + """ + def getNodes(requestor, service, nodeIdentifier): + raise Exception("Unexpected call to getNodes") + + def cb(items): + self.assertEqual([], items) + + self.service.hideNodes = True + self.resource.getNodes = getNodes + d = self.service.getDiscoItems(JID('user@example.org/home'), + JID('pubsub.example.org'), + '') + d.addCallback(cb) + return d + + + def test_getDiscoItemsNonRoot(self): + """ + Test getDiscoItems on a non-root node. + """ + def getNodes(requestor, service, nodeIdentifier): + return defer.succeed(['node1', 'node2']) + + def cb(items): + self.assertEqual(2, len(items)) + + self.resource.getNodes = getNodes + d = self.service.getDiscoItems(JID('user@example.org/home'), + JID('pubsub.example.org'), + 'test') + d.addCallback(cb) + return d + + + def test_on_publish(self): + """ + A publish request should result in L{PubSubService.publish} being + called. + """ + + xml = """ + + + + + + """ + + def publish(request): + return defer.succeed(None) + + self.resource.publish = publish + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + return self.handleRequest(xml) + + + def test_on_subscribe(self): + """ + A successful subscription should return the current subscription. + """ + + xml = """ + + + + + + """ + + def subscribe(request): + return defer.succeed(pubsub.Subscription(request.nodeIdentifier, + request.subscriber, + 'subscribed')) + + def cb(element): + self.assertEqual('pubsub', element.name) + self.assertEqual(NS_PUBSUB, element.uri) + subscription = element.subscription + self.assertEqual(NS_PUBSUB, subscription.uri) + self.assertEqual('test', subscription['node']) + self.assertEqual('user@example.org/Home', subscription['jid']) + self.assertEqual('subscribed', subscription['subscription']) + + self.resource.subscribe = subscribe + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_subscribeEmptyNode(self): + """ + A successful subscription on root node should return no node attribute. + """ + + xml = """ + + + + + + """ + + def subscribe(request): + return defer.succeed(pubsub.Subscription(request.nodeIdentifier, + request.subscriber, + 'subscribed')) + + def cb(element): + self.assertFalse(element.subscription.hasAttribute('node')) + + self.resource.subscribe = subscribe + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_subscribeSubscriptionIdentifier(self): + """ + If a subscription returns a subid, this should be available. + """ + + xml = """ + + + + + + """ + + def subscribe(request): + subscription = pubsub.Subscription(request.nodeIdentifier, + request.subscriber, + 'subscribed', + subscriptionIdentifier='1234') + return defer.succeed(subscription) + + def cb(element): + self.assertEqual('1234', element.subscription.getAttribute('subid')) + + self.resource.subscribe = subscribe + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_unsubscribe(self): + """ + A successful unsubscription should return an empty response. + """ + + xml = """ + + + + + + """ + + def unsubscribe(request): + return defer.succeed(None) + + def cb(element): + self.assertIdentical(None, element) + + self.resource.unsubscribe = unsubscribe + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_unsubscribeSubscriptionIdentifier(self): + """ + A successful unsubscription with subid should return an empty response. + """ + + xml = """ + + + + + + """ + + def unsubscribe(request): + self.assertEqual('1234', request.subscriptionIdentifier) + return defer.succeed(None) + + def cb(element): + self.assertIdentical(None, element) + + self.resource.unsubscribe = unsubscribe + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_optionsGet(self): + """ + Getting subscription options is not supported. + """ + + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_on_optionsSet(self): + """ + Setting subscription options is not supported. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#subscribe_options + + 1 + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_on_subscriptions(self): + """ + A subscriptions request should result in + L{PubSubService.subscriptions} being called and the result prepared + for the response. + """ + + xml = """ + + + + + + """ + + def subscriptions(request): + subscription = pubsub.Subscription('test', JID('user@example.org'), + 'subscribed') + return defer.succeed([subscription]) + + def cb(element): + self.assertEqual('pubsub', element.name) + self.assertEqual(NS_PUBSUB, element.uri) + self.assertEqual(NS_PUBSUB, element.subscriptions.uri) + children = list(element.subscriptions.elements()) + self.assertEqual(1, len(children)) + subscription = children[0] + self.assertEqual('subscription', subscription.name) + self.assertEqual(NS_PUBSUB, subscription.uri, NS_PUBSUB) + self.assertEqual('user@example.org', subscription['jid']) + self.assertEqual('test', subscription['node']) + self.assertEqual('subscribed', subscription['subscription']) + + self.resource.subscriptions = subscriptions + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_subscriptionsWithSubscriptionIdentifier(self): + """ + A subscriptions request response should include subids, if set. + """ + + xml = """ + + + + + + """ + + def subscriptions(request): + subscription = pubsub.Subscription('test', JID('user@example.org'), + 'subscribed', + subscriptionIdentifier='1234') + return defer.succeed([subscription]) + + def cb(element): + subscription = element.subscriptions.subscription + self.assertEqual('1234', subscription['subid']) + + self.resource.subscriptions = subscriptions + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_affiliations(self): + """ + A subscriptions request should result in + L{PubSubService.affiliations} being called and the result prepared + for the response. + """ + + xml = """ + + + + + + """ + + def affiliations(request): + affiliation = ('test', 'owner') + return defer.succeed([affiliation]) + + def cb(element): + self.assertEqual('pubsub', element.name) + self.assertEqual(NS_PUBSUB, element.uri) + self.assertEqual(NS_PUBSUB, element.affiliations.uri) + children = list(element.affiliations.elements()) + self.assertEqual(1, len(children)) + affiliation = children[0] + self.assertEqual('affiliation', affiliation.name) + self.assertEqual(NS_PUBSUB, affiliation.uri) + self.assertEqual('test', affiliation['node']) + self.assertEqual('owner', affiliation['affiliation']) + + self.resource.affiliations = affiliations + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_create(self): + """ + Replies to create node requests don't return the created node. + """ + + xml = """ + + + + + + """ + + def create(request): + return defer.succeed(request.nodeIdentifier) + + def cb(element): + self.assertIdentical(None, element) + + self.resource.create = create + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_createChanged(self): + """ + Replies to create node requests return the created node if changed. + """ + + xml = """ + + + + + + """ + + def create(request): + return defer.succeed(u'myrenamednode') + + def cb(element): + self.assertEqual('pubsub', element.name) + self.assertEqual(NS_PUBSUB, element.uri) + self.assertEqual(NS_PUBSUB, element.create.uri) + self.assertEqual(u'myrenamednode', + element.create.getAttribute('node')) + + self.resource.create = create + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_createInstant(self): + """ + Replies to create instant node requests return the created node. + """ + + xml = """ + + + + + + """ + + def create(request): + return defer.succeed(u'random') + + def cb(element): + self.assertEqual('pubsub', element.name) + self.assertEqual(NS_PUBSUB, element.uri) + self.assertEqual(NS_PUBSUB, element.create.uri) + self.assertEqual(u'random', element.create.getAttribute('node')) + + self.resource.create = create + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_createWithConfig(self): + """ + On a node create with configuration request the Data Form is parsed and + L{PubSubResource.create} is called with the passed options. + """ + + xml = """ + + + + + + + http://jabber.org/protocol/pubsub#node_config + + 0 + 1 + + + + + """ + + def getConfigurationOptions(): + return { + "pubsub#persist_items": + {"type": "boolean", + "label": "Persist items to storage"}, + "pubsub#deliver_payloads": + {"type": "boolean", + "label": "Deliver payloads with event notifications"} + } + + def create(request): + self.assertEqual({'pubsub#deliver_payloads': False, + 'pubsub#persist_items': True}, + request.options.getValues()) + return defer.succeed(None) + + self.resource.getConfigurationOptions = getConfigurationOptions + self.resource.create = create + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + return self.handleRequest(xml) + + + def test_on_default(self): + """ + A default request returns default options filtered by available fields. + """ + + xml = """ + + + + + + """ + fieldDefs = { + "pubsub#persist_items": + {"type": "boolean", + "label": "Persist items to storage"}, + "pubsub#deliver_payloads": + {"type": "boolean", + "label": "Deliver payloads with event notifications"} + } + + def getConfigurationOptions(): + return fieldDefs + + def default(request): + return defer.succeed({'pubsub#persist_items': 'false', + 'x-myfield': '1'}) + + def cb(element): + self.assertEquals('pubsub', element.name) + self.assertEquals(NS_PUBSUB_OWNER, element.uri) + self.assertEquals(NS_PUBSUB_OWNER, element.default.uri) + form = data_form.Form.fromElement(element.default.x) + self.assertEquals(NS_PUBSUB_NODE_CONFIG, form.formNamespace) + form.typeCheck(fieldDefs) + self.assertIn('pubsub#persist_items', form.fields) + self.assertFalse(form.fields['pubsub#persist_items'].value) + self.assertNotIn('x-myfield', form.fields) + + self.resource.getConfigurationOptions = getConfigurationOptions + self.resource.default = default + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_defaultUnknownNodeType(self): + """ + Unknown node types yield non-acceptable. + + Both C{getConfigurationOptions} and C{default} must not be called. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + + unknown + + + + + + + """ + + def getConfigurationOptions(): + self.fail("Unexpected call to getConfigurationOptions") + + def default(request): + self.fail("Unexpected call to default") + + def cb(result): + self.assertEquals('not-acceptable', result.condition) + + self.resource.getConfigurationOptions = getConfigurationOptions + self.resource.default = default + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_on_configureGet(self): + """ + On a node configuration get + requestL{PubSubResource.configureGet} is called and results in a + data form with the configuration. + """ + + xml = """ + + + + + + """ + + def getConfigurationOptions(): + return { + "pubsub#persist_items": + {"type": "boolean", + "label": "Persist items to storage"}, + "pubsub#deliver_payloads": + {"type": "boolean", + "label": "Deliver payloads with event notifications"}, + "pubsub#owner": + {"type": "jid-single", + "label": "Owner of the node"} + } + + def configureGet(request): + return defer.succeed({'pubsub#deliver_payloads': '0', + 'pubsub#persist_items': '1', + 'pubsub#owner': JID('user@example.org'), + 'x-myfield': 'a'}) + + def cb(element): + self.assertEqual('pubsub', element.name) + self.assertEqual(NS_PUBSUB_OWNER, element.uri) + self.assertEqual(NS_PUBSUB_OWNER, element.configure.uri) + form = data_form.Form.fromElement(element.configure.x) + self.assertEqual(NS_PUBSUB_NODE_CONFIG, form.formNamespace) + fields = form.fields + + self.assertIn('pubsub#deliver_payloads', fields) + field = fields['pubsub#deliver_payloads'] + self.assertEqual('boolean', field.fieldType) + field.typeCheck() + self.assertEqual(False, field.value) + + self.assertIn('pubsub#persist_items', fields) + field = fields['pubsub#persist_items'] + self.assertEqual('boolean', field.fieldType) + field.typeCheck() + self.assertEqual(True, field.value) + + self.assertIn('pubsub#owner', fields) + field = fields['pubsub#owner'] + self.assertEqual('jid-single', field.fieldType) + field.typeCheck() + self.assertEqual(JID('user@example.org'), field.value) + + self.assertNotIn('x-myfield', fields) + + self.resource.getConfigurationOptions = getConfigurationOptions + self.resource.configureGet = configureGet + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_configureSet(self): + """ + On a node configuration set request the Data Form is parsed and + L{PubSubResource.configureSet} is called with the passed options. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + 0 + 1 + + + + + """ + + def getConfigurationOptions(): + return { + "pubsub#persist_items": + {"type": "boolean", + "label": "Persist items to storage"}, + "pubsub#deliver_payloads": + {"type": "boolean", + "label": "Deliver payloads with event notifications"} + } + + def configureSet(request): + self.assertEqual({'pubsub#deliver_payloads': False, + 'pubsub#persist_items': True}, + request.options.getValues()) + return defer.succeed(None) + + self.resource.getConfigurationOptions = getConfigurationOptions + self.resource.configureSet = configureSet + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + return self.handleRequest(xml) + + + def test_on_configureSetCancel(self): + """ + The node configuration is cancelled, + L{PubSubResource.configureSet} not called. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + + + + + """ + + def configureSet(request): + self.fail("Unexpected call to setConfiguration") + + self.resource.configureSet = configureSet + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + return self.handleRequest(xml) + + + def test_on_configureSetIgnoreUnknown(self): + """ + On a node configuration set request unknown fields should be ignored. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + 0 + 1 + + + + + """ + + def getConfigurationOptions(): + return { + "pubsub#persist_items": + {"type": "boolean", + "label": "Persist items to storage"}, + "pubsub#deliver_payloads": + {"type": "boolean", + "label": "Deliver payloads with event notifications"} + } + + def configureSet(request): + self.assertEquals(['pubsub#deliver_payloads'], + request.options.keys()) + + self.resource.getConfigurationOptions = getConfigurationOptions + self.resource.configureSet = configureSet + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + return self.handleRequest(xml) + + + def test_on_configureSetBadFormType(self): + """ + On a node configuration set request unknown fields should be ignored. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + 0 + 1 + + + + + """ + + def cb(result): + self.assertEquals('bad-request', result.condition) + self.assertEqual("Unexpected form type 'result'", result.text) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_on_items(self): + """ + On a items request, return all items for the given node. + """ + xml = """ + + + + + + """ + + def items(request): + return defer.succeed([pubsub.Item('current')]) + + def cb(element): + self.assertEqual(NS_PUBSUB, element.uri) + self.assertEqual(NS_PUBSUB, element.items.uri) + self.assertEqual(1, len(element.items.children)) + item = element.items.children[-1] + self.assertTrue(domish.IElement.providedBy(item)) + self.assertEqual('item', item.name) + self.assertEqual(NS_PUBSUB, item.uri) + self.assertEqual('current', item['id']) + + self.resource.items = items + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_retract(self): + """ + A retract request should result in L{PubSubResource.retract} + being called. + """ + + xml = """ + + + + + + + + + """ + + def retract(request): + return defer.succeed(None) + + self.resource.retract = retract + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + return self.handleRequest(xml) + + + def test_on_purge(self): + """ + A purge request should result in L{PubSubResource.purge} being + called. + """ + + xml = """ + + + + + + """ + + def purge(request): + return defer.succeed(None) + + self.resource.purge = purge + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + return self.handleRequest(xml) + + + def test_on_delete(self): + """ + A delete request should result in L{PubSubResource.delete} being + called. + """ + + xml = """ + + + + + + """ + + def delete(request): + return defer.succeed(None) + + self.resource.delete = delete + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + return self.handleRequest(xml) + + + def test_notifyPublish(self): + """ + Publish notifications are sent to the subscribers. + """ + subscriber = JID('user@example.org') + subscriptions = [pubsub.Subscription('test', subscriber, 'subscribed')] + items = [pubsub.Item('current')] + notifications = [(subscriber, subscriptions, items)] + self.service.notifyPublish(JID('pubsub.example.org'), 'test', + notifications) + message = self.stub.output[-1] + + self.assertEquals('message', message.name) + self.assertIdentical(None, message.uri) + self.assertEquals('user@example.org', message['to']) + self.assertEquals('pubsub.example.org', message['from']) + self.assertTrue(message.event) + self.assertEquals(NS_PUBSUB_EVENT, message.event.uri) + self.assertTrue(message.event.items) + self.assertEquals(NS_PUBSUB_EVENT, message.event.items.uri) + self.assertTrue(message.event.items.hasAttribute('node')) + self.assertEquals('test', message.event.items['node']) + itemElements = list(domish.generateElementsQNamed( + message.event.items.children, 'item', NS_PUBSUB_EVENT)) + self.assertEquals(1, len(itemElements)) + self.assertEquals('current', itemElements[0].getAttribute('id')) + + + def test_notifyPublishCollection(self): + """ + Publish notifications are sent to the subscribers of collections. + + The node the item was published to is on the C{items} element, while + the subscribed-to node is in the C{'Collections'} SHIM header. + """ + subscriber = JID('user@example.org') + subscriptions = [pubsub.Subscription('', subscriber, 'subscribed')] + items = [pubsub.Item('current')] + notifications = [(subscriber, subscriptions, items)] + self.service.notifyPublish(JID('pubsub.example.org'), 'test', + notifications) + message = self.stub.output[-1] + + self.assertTrue(message.event.items.hasAttribute('node')) + self.assertEquals('test', message.event.items['node']) + headers = shim.extractHeaders(message) + self.assertIn('Collection', headers) + self.assertIn('', headers['Collection']) + + + def test_notifyDelete(self): + """ + Subscribers should be sent a delete notification. + """ + subscriptions = [JID('user@example.org')] + self.service.notifyDelete(JID('pubsub.example.org'), 'test', + subscriptions) + message = self.stub.output[-1] + + self.assertEquals('message', message.name) + self.assertIdentical(None, message.uri) + self.assertEquals('user@example.org', message['to']) + self.assertEquals('pubsub.example.org', message['from']) + self.assertTrue(message.event) + self.assertEqual(NS_PUBSUB_EVENT, message.event.uri) + self.assertTrue(message.event.delete) + self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.uri) + self.assertTrue(message.event.delete.hasAttribute('node')) + self.assertEqual('test', message.event.delete['node']) + + + def test_notifyDeleteRedirect(self): + """ + Subscribers should be sent a delete notification with redirect. + """ + redirectURI = 'xmpp:pubsub.example.org?;node=test2' + subscriptions = [JID('user@example.org')] + self.service.notifyDelete(JID('pubsub.example.org'), 'test', + subscriptions, redirectURI) + message = self.stub.output[-1] + + self.assertEquals('message', message.name) + self.assertIdentical(None, message.uri) + self.assertEquals('user@example.org', message['to']) + self.assertEquals('pubsub.example.org', message['from']) + self.assertTrue(message.event) + self.assertEqual(NS_PUBSUB_EVENT, message.event.uri) + self.assertTrue(message.event.delete) + self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.uri) + self.assertTrue(message.event.delete.hasAttribute('node')) + self.assertEqual('test', message.event.delete['node']) + self.assertTrue(message.event.delete.redirect) + self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.redirect.uri) + self.assertTrue(message.event.delete.redirect.hasAttribute('uri')) + self.assertEqual(redirectURI, message.event.delete.redirect['uri']) + + + def test_on_subscriptionsGet(self): + """ + Getting subscription options is not supported. + """ + + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('manage-subscriptions', + result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_on_subscriptionsSet(self): + """ + Setting subscription options is not supported. + """ + + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('manage-subscriptions', + result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_on_affiliationsGet(self): + """ + Getting node affiliations should have. + """ + + xml = """ + + + + + + """ + + def affiliationsGet(request): + self.assertEquals('test', request.nodeIdentifier) + return defer.succeed({JID('user@example.org'): 'owner'}) + + def cb(element): + self.assertEquals(u'pubsub', element.name) + self.assertEquals(NS_PUBSUB_OWNER, element.uri) + self.assertEquals(NS_PUBSUB_OWNER, element.affiliations.uri) + self.assertEquals(u'test', element.affiliations[u'node']) + children = list(element.affiliations.elements()) + self.assertEquals(1, len(children)) + affiliation = children[0] + self.assertEquals(u'affiliation', affiliation.name) + self.assertEquals(NS_PUBSUB_OWNER, affiliation.uri) + self.assertEquals(u'user@example.org', affiliation[u'jid']) + self.assertEquals(u'owner', affiliation[u'affiliation']) + + self.resource.affiliationsGet = affiliationsGet + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_affiliationsGetEmptyNode(self): + """ + Getting node affiliations without node should assume empty node. + """ + + xml = """ + + + + + + """ + + def affiliationsGet(request): + self.assertEqual('', request.nodeIdentifier) + return defer.succeed({}) + + def cb(element): + self.assertFalse(element.affiliations.hasAttribute(u'node')) + + self.resource.affiliationsGet = affiliationsGet + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_on_affiliationsSet(self): + """ + Setting node affiliations has the affiliations to be modified. + """ + + xml = """ + + + + + + + + """ + + def affiliationsSet(request): + self.assertEquals(u'test', request.nodeIdentifier) + otherJID = JID(u'other@example.org') + self.assertIn(otherJID, request.affiliations) + self.assertEquals(u'publisher', request.affiliations[otherJID]) + + self.resource.affiliationsSet = affiliationsSet + return self.handleRequest(xml) + + + def test_on_affiliationsSetBareJID(self): + """ + Affiliations are always on the bare JID. + """ + + xml = """ + + + + + + + + """ + + def affiliationsSet(request): + otherJID = JID(u'other@example.org') + self.assertIn(otherJID, request.affiliations) + + self.resource.affiliationsSet = affiliationsSet + return self.handleRequest(xml) + + + def test_on_affiliationsSetMultipleForSameEntity(self): + """ + Setting node affiliations can only have one item per entity. + """ + + xml = """ + + + + + + + + + """ + + def cb(result): + self.assertEquals('bad-request', result.condition) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_on_affiliationsSetMissingJID(self): + """ + Setting node affiliations must include a JID per affiliation. + """ + + xml = """ + + + + + + + + """ + + def cb(result): + self.assertEquals('bad-request', result.condition) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_on_affiliationsSetMissingAffiliation(self): + """ + Setting node affiliations must include an affiliation. + """ + + xml = """ + + + + + + + + """ + + def cb(result): + self.assertEquals('bad-request', result.condition) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + +class PubSubServiceWithoutResourceTest(unittest.TestCase, TestableRequestHandlerMixin): + + def setUp(self): + self.stub = XmlStreamStub() + self.service = pubsub.PubSubService() + self.service.send = self.stub.xmlstream.send + + + def test_getDiscoInfo(self): + """ + Test getDiscoInfo calls getNodeInfo and returns some minimal info. + """ + def cb(info): + discoInfo = disco.DiscoInfo() + for item in info: + discoInfo.append(item) + self.assertIn(('pubsub', 'service'), discoInfo.identities) + self.assertIn(disco.NS_DISCO_ITEMS, discoInfo.features) + + d = self.service.getDiscoInfo(JID('user@example.org/home'), + JID('pubsub.example.org'), '') + d.addCallback(cb) + return d + + + def test_publish(self): + """ + Non-overridden L{PubSubService.publish} yields unsupported error. + """ + + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('publish', result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_subscribe(self): + """ + Non-overridden L{PubSubService.subscribe} yields unsupported error. + """ + + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('subscribe', result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_unsubscribe(self): + """ + Non-overridden L{PubSubService.unsubscribe} yields unsupported error. + """ + + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('subscribe', result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_subscriptions(self): + """ + Non-overridden L{PubSubService.subscriptions} yields unsupported error. + """ + + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('retrieve-subscriptions', + result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_affiliations(self): + """ + Non-overridden L{PubSubService.affiliations} yields unsupported error. + """ + + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('retrieve-affiliations', + result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_create(self): + """ + Non-overridden L{PubSubService.create} yields unsupported error. + """ + + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('create-nodes', result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_getDefaultConfiguration(self): + """ + Non-overridden L{PubSubService.getDefaultConfiguration} yields + unsupported error. + """ + + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('retrieve-default', result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_getConfiguration(self): + """ + Non-overridden L{PubSubService.getConfiguration} yields unsupported + error. + """ + + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('config-node', result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_setConfiguration(self): + """ + Non-overridden L{PubSubService.setConfiguration} yields unsupported + error. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + 0 + 1 + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('config-node', result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_setConfigurationOptionsDict(self): + """ + Options should be passed as a dictionary, not a form. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + 0 + 1 + + + + + """ + + def getConfigurationOptions(): + return { + "pubsub#persist_items": + {"type": "boolean", + "label": "Persist items to storage"}, + "pubsub#deliver_payloads": + {"type": "boolean", + "label": "Deliver payloads with event notifications"} + } + + def setConfiguration(requestor, service, nodeIdentifier, options): + self.assertIn('pubsub#deliver_payloads', options) + self.assertFalse(options['pubsub#deliver_payloads']) + self.assertIn('pubsub#persist_items', options) + self.assertTrue(options['pubsub#persist_items']) + + self.service.getConfigurationOptions = getConfigurationOptions + self.service.setConfiguration = setConfiguration + return self.handleRequest(xml) + + + def test_items(self): + """ + Non-overridden L{PubSubService.items} yields unsupported error. + """ + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('retrieve-items', result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_retract(self): + """ + Non-overridden L{PubSubService.retract} yields unsupported error. + """ + xml = """ + + + + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('retract-items', result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_purge(self): + """ + Non-overridden L{PubSubService.purge} yields unsupported error. + """ + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('purge-nodes', result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_delete(self): + """ + Non-overridden L{PubSubService.delete} yields unsupported error. + """ + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('delete-nodes', result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_unknown(self): + """ + Unknown verb yields unsupported error. + """ + xml = """ + + + + + + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + +class PubSubResourceTest(unittest.TestCase): + + def setUp(self): + self.resource = pubsub.PubSubResource() + + + def test_interface(self): + """ + Do instances of L{pubsub.PubSubResource} provide L{iwokkel.IPubSubResource}? + """ + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + + + def test_getNodes(self): + """ + Default getNodes returns an empty list. + """ + def cb(nodes): + self.assertEquals([], nodes) + + d = self.resource.getNodes(JID('user@example.org/home'), + JID('pubsub.example.org'), + '') + d.addCallback(cb) + return d + + + def test_publish(self): + """ + Non-overridden L{PubSubResource.publish} yields unsupported + error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('publish', result.appCondition['feature']) + + d = self.resource.publish(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_subscribe(self): + """ + Non-overridden subscriptions yields unsupported error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('subscribe', result.appCondition['feature']) + + d = self.resource.subscribe(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_unsubscribe(self): + """ + Non-overridden unsubscribe yields unsupported error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('subscribe', result.appCondition['feature']) + + d = self.resource.unsubscribe(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_subscriptions(self): + """ + Non-overridden subscriptions yields unsupported error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('retrieve-subscriptions', + result.appCondition['feature']) + + d = self.resource.subscriptions(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_affiliations(self): + """ + Non-overridden affiliations yields unsupported error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('retrieve-affiliations', + result.appCondition['feature']) + + d = self.resource.affiliations(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_create(self): + """ + Non-overridden create yields unsupported error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('create-nodes', result.appCondition['feature']) + + d = self.resource.create(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_default(self): + """ + Non-overridden default yields unsupported error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('retrieve-default', + result.appCondition['feature']) + + d = self.resource.default(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_configureGet(self): + """ + Non-overridden configureGet yields unsupported + error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('config-node', result.appCondition['feature']) + + d = self.resource.configureGet(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_configureSet(self): + """ + Non-overridden configureSet yields unsupported error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('config-node', result.appCondition['feature']) + + d = self.resource.configureSet(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_items(self): + """ + Non-overridden items yields unsupported error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('retrieve-items', result.appCondition['feature']) + + d = self.resource.items(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_retract(self): + """ + Non-overridden retract yields unsupported error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('retract-items', result.appCondition['feature']) + + d = self.resource.retract(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_purge(self): + """ + Non-overridden purge yields unsupported error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('purge-nodes', result.appCondition['feature']) + + d = self.resource.purge(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_delete(self): + """ + Non-overridden delete yields unsupported error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('delete-nodes', result.appCondition['feature']) + + d = self.resource.delete(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_affiliationsGet(self): + """ + Non-overridden owner affiliations get yields unsupported error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('modify-affiliations', + result.appCondition['feature']) + + d = self.resource.affiliationsGet(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_affiliationsSet(self): + """ + Non-overridden owner affiliations set yields unsupported error. + """ + + def cb(result): + self.assertEquals('feature-not-implemented', result.condition) + self.assertEquals('unsupported', result.appCondition.name) + self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) + self.assertEquals('modify-affiliations', + result.appCondition['feature']) + + d = self.resource.affiliationsSet(pubsub.PubSubRequest()) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d diff -r 7430d1f6db22 -r c8cb4e867897 sat_tmp/wokkel/test/test_rsm.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat_tmp/wokkel/test/test_rsm.py Thu Nov 02 22:50:59 2017 +0100 @@ -0,0 +1,662 @@ +# Copyright (c) Adrien Cossa. +# See LICENSE for details. + +""" +Tests for L{wokkel.rsm}. +""" + +from zope.interface import verify + +from twisted.trial import unittest +from twisted.words.xish import domish +from twisted.words.protocols.jabber.jid import JID +from twisted.words.protocols.jabber.xmlstream import toResponse +from twisted.internet import defer + +from wokkel.generic import parseXml +from wokkel import iwokkel +from wokkel.test.helpers import XmlStreamStub, TestableRequestHandlerMixin + +from sat.tmp.wokkel import pubsub +from sat.tmp.wokkel.rsm import NS_RSM, RSMRequest, RSMResponse, PubSubClient, PubSubService + +import uuid + +RSMResponse.__eq__ = lambda self, other: self.first == other.first and\ + self.last == other.last and\ + self.index == other.index and\ + self.count == other.count + +class RSMRequestTest(unittest.TestCase): + """ + Tests for L{rsm.RSMRequest}. + """ + + def test___init__(self): + """ + Fail to initialize a RSMRequest with wrong attribute values. + """ + self.assertRaises(AssertionError, RSMRequest, index=371, after=u'test') + self.assertRaises(AssertionError, RSMRequest, index=371, before=u'test') + self.assertRaises(AssertionError, RSMRequest, before=117) + self.assertRaises(AssertionError, RSMRequest, after=312) + self.assertRaises(AssertionError, RSMRequest, after=u'117', before=u'312') + + def test_parse(self): + """ + Parse a request element asking for the first page. + """ + xml = """ + + Pete + + 1 + + + """ + request = RSMRequest.fromElement(parseXml(xml)) + self.assertEqual(1, request.max) + self.assertIdentical(None, request.index) + self.assertIdentical(None, request.after) + self.assertIdentical(None, request.before) + + def test_parseSecondPage(self): + """ + Parse a request element asking for a next page. + """ + xml = """ + + Pete + + 3 + peterpan@neverland.lit + + + """ + request = RSMRequest.fromElement(parseXml(xml)) + self.assertEqual(3, request.max) + self.assertIdentical(None, request.index) + self.assertEqual(u'peterpan@neverland.lit', request.after) + self.assertIdentical(None, request.before) + + def test_parsePreviousPage(self): + """ + Parse a request element asking for a previous page. + """ + xml = """ + + Pete + + 5 + peterpan@pixyland.org + + + """ + request = RSMRequest.fromElement(parseXml(xml)) + self.assertEqual(5, request.max) + self.assertIdentical(None, request.index) + self.assertIdentical(None, request.after) + self.assertEqual(u'peterpan@pixyland.org', request.before) + + def test_parseLastPage(self): + """ + Parse a request element asking for the last page. + """ + xml = """ + + Pete + + 7 + + + + """ + request = RSMRequest.fromElement(parseXml(xml)) + self.assertEqual(7, request.max) + self.assertIdentical(None, request.index) + self.assertIdentical(None, request.after) + self.assertEqual('', request.before) + + def test_parseOutOfOrderPage(self): + """ + Parse a request element asking for a page out of order. + """ + xml = """ + + Pete + + 9 + 371 + + + """ + request = RSMRequest.fromElement(parseXml(xml)) + self.assertEqual(9, request.max) + self.assertEqual(371, request.index) + self.assertIdentical(None, request.after) + self.assertIdentical(None, request.before) + + def test_parseItemCount(self): + """ + Parse a request element asking for the items count. + """ + xml = """ + + Pete + + 0 + + + """ + request = RSMRequest.fromElement(parseXml(xml)) + self.assertEqual(0, request.max) + self.assertIdentical(None, request.index) + self.assertIdentical(None, request.after) + self.assertIdentical(None, request.before) + + def test_render(self): + """ + Embed a page request in the element. + """ + element = domish.Element(('jabber:iq:search', 'query')) + element.addElement('items')['max_items'] = u'10' + RSMRequest(1).render(element) + + self.assertEqual(u'10', element.items['max_items']) # not changed + + self.assertEqual(NS_RSM, element.set.uri) + self.assertEqual(u'1', ''.join(element.set.max.children)) + self.assertIdentical(None, element.set.after) + self.assertIdentical(None, element.set.before) + self.assertIdentical(None, element.set.index) + + def test_renderPubSub(self): + """ + Embed a page request in the pubsub element. + """ + element = domish.Element((pubsub.NS_PUBSUB, 'pubsub')) + element.addElement('items')['max_items'] = u'10' + RSMRequest(3).render(element) + + self.assertEqual(u'10', element.items['max_items']) # not changed + + self.assertEqual(NS_RSM, element.set.uri) + self.assertEqual(u'3', ''.join(element.set.max.children)) + self.assertIdentical(None, element.set.after) + self.assertIdentical(None, element.set.before) + self.assertIdentical(None, element.set.index) + + def test_renderItems(self): + """ + Embed a page request in the element, specify items. + """ + element = domish.Element(('jabber:iq:search', 'query')) + RSMRequest(5, index=127).render(element) + self.assertEqual(NS_RSM, element.set.uri) + self.assertEqual(u'5', ''.join(element.set.max.children)) + self.assertIdentical(None, element.set.after) + self.assertIdentical(None, element.set.before) + self.assertEqual(u'127', ''.join(element.set.index.children)) + + def test_renderAfter(self): + """ + Embed a page request in the element, specify after. + """ + element = domish.Element(('jabber:iq:search', 'query')) + RSMRequest(5, after=u'test').render(element) + self.assertEqual(NS_RSM, element.set.uri) + self.assertEqual(u'5', ''.join(element.set.max.children)) + self.assertEqual(u'test', ''.join(element.set.after.children)) + self.assertIdentical(None, element.set.before) + self.assertIdentical(None, element.set.index) + + def test_renderBefore(self): + """ + Embed a page request in the element, specify before. + """ + element = domish.Element(('jabber:iq:search', 'query')) + RSMRequest(5, before=u'test').render(element) + self.assertEqual(NS_RSM, element.set.uri) + self.assertEqual(u'5', ''.join(element.set.max.children)) + self.assertIdentical(None, element.set.after) + self.assertEqual(u'test', ''.join(element.set.before.children)) + self.assertIdentical(None, element.set.index) + + +class RSMResponseTest(unittest.TestCase): + """ + Tests for L{rsm.RSMResponse}. + """ + + def test___init__(self): + """ + Fail to initialize a RSMResponse with wrong attribute values. + """ + self.assertRaises(AssertionError, RSMResponse, index=127, first=u'127') + self.assertRaises(AssertionError, RSMResponse, index=127, last=u'351') + + def test_parse(self): + """ + Parse a response element returning a page. + """ + xml = """ + + + stpeter@jabber.org + peterpan@neverland.lit + 800 + + + """ + response = RSMResponse.fromElement(parseXml(xml)) + self.assertEqual(800, response.count) + self.assertEqual(20, response.index) + self.assertEqual(u'stpeter@jabber.org', response.first) + self.assertEqual(u'peterpan@neverland.lit', response.last) + + def test_parseEmptySet(self): + """ + Parse a response element returning an empty set. + """ + xml = """ + + + 800 + + + """ + response = RSMResponse.fromElement(parseXml(xml)) + self.assertEqual(800, response.count) + self.assertIdentical(None, response.first) + self.assertIdentical(None, response.last) + self.assertIdentical(None, response.index) + + def test_render(self): + """ + Embed a page response in the element. + """ + element = domish.Element(('jabber:iq:search', 'query')) + RSMResponse(u'stpeter@jabber.org', u'peterpan@neverland.lit', 20, 800).render(element) + + self.assertEqual(NS_RSM, element.set.uri) + self.assertEqual(u'800', ''.join(element.set.count.children)) + self.assertEqual(u'stpeter@jabber.org', + ''.join(element.set.first.children)) + self.assertEqual(u'peterpan@neverland.lit', + ''.join(element.set.last.children)) + self.assertEqual(u'20', element.set.first['index']) + + def test_renderEmptySet(self): + """ + Embed a page response in the element, for empty set. + """ + element = domish.Element(('jabber:iq:search', 'query')) + RSMResponse(count=800).render(element) + + self.assertEqual(NS_RSM, element.set.uri) + self.assertEqual(u'800', ''.join(element.set.count.children)) + self.assertIdentical(None, element.set.first) + self.assertIdentical(None, element.set.last) + + +class PubSubClientTest(unittest.TestCase): + """ + Tests for L{rsm.PubSubClient}. + """ + timeout = 2 + + def setUp(self): + self.stub = XmlStreamStub() + self.protocol = PubSubClient() + self.protocol.xmlstream = self.stub.xmlstream + self.protocol.connectionInitialized() + + def test_items(self): + """ + Test sending items request to get the first page. + """ + def cb(response): + items, rsm = response + self.assertEquals(2, len(items)) + self.assertEquals([item1, item2], items) + self.assertEquals(rsm, RSMResponse('item1', 'item2', 0, 800)) + + d = self.protocol.items(JID('pubsub.example.org'), 'test', + rsm_request=RSMRequest(2)) + d.addCallback(cb) + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('get', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(pubsub.NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'items', pubsub.NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + + set_elts = list(domish.generateElementsQNamed(iq.pubsub.children, + 'set', NS_RSM)) + self.assertEquals(1, len(set_elts)) + set_elt = set_elts[0] + self.assertEquals(u'2', ''.join(set_elt.max.children)) + + response = toResponse(iq, 'result') + items = response.addElement((pubsub.NS_PUBSUB, + 'pubsub')).addElement('items') + items['node'] = 'test' + item1 = items.addElement('item') + item1['id'] = 'item1' + item2 = items.addElement('item') + item2['id'] = 'item2' + RSMResponse(u'item1', u'item2', 0, 800).render(response.pubsub) + self.stub.send(response) + + return d + + def test_itemsAfter(self): + """ + Test sending items request to get the next page. + """ + def cb(response): + items, rsm = response + self.assertEquals(2, len(items)) + self.assertEquals([item1, item2], items) + self.assertEquals(rsm, RSMResponse('item3', 'item4', 2, 800)) + + d = self.protocol.items(JID('pubsub.example.org'), 'test', + rsm_request=RSMRequest(2, after=u'item2')) + d.addCallback(cb) + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('get', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(pubsub.NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'items', pubsub.NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + + set_elts = list(domish.generateElementsQNamed(iq.pubsub.children, + 'set', NS_RSM)) + self.assertEquals(1, len(set_elts)) + set_elt = set_elts[0] + self.assertEquals(u'2', ''.join(set_elt.max.children)) + self.assertEquals(u'item2', ''.join(set_elt.after.children)) + + response = toResponse(iq, 'result') + items = response.addElement((pubsub.NS_PUBSUB, + 'pubsub')).addElement('items') + items['node'] = 'test' + item1 = items.addElement('item') + item1['id'] = 'item3' + item2 = items.addElement('item') + item2['id'] = 'item4' + RSMResponse(u'item3', u'item4', 2, 800).render(response.pubsub) + self.stub.send(response) + + return d + + def test_itemsBefore(self): + """ + Test sending items request to get the previous page. + """ + def cb(response): + items, rsm = response + self.assertEquals(2, len(items)) + self.assertEquals([item1, item2], items) + self.assertEquals(rsm, RSMResponse('item1', 'item2', 0, 800)) + + d = self.protocol.items(JID('pubsub.example.org'), 'test', + rsm_request=RSMRequest(2, before=u'item3')) + d.addCallback(cb) + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('get', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(pubsub.NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'items', pubsub.NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + + set_elts = list(domish.generateElementsQNamed(iq.pubsub.children, + 'set', NS_RSM)) + self.assertEquals(1, len(set_elts)) + set_elt = set_elts[0] + self.assertEquals(u'2', ''.join(set_elt.max.children)) + self.assertEquals(u'item3', ''.join(set_elt.before.children)) + + response = toResponse(iq, 'result') + items = response.addElement((pubsub.NS_PUBSUB, + 'pubsub')).addElement('items') + items['node'] = 'test' + item1 = items.addElement('item') + item1['id'] = 'item1' + item2 = items.addElement('item') + item2['id'] = 'item2' + RSMResponse(u'item1', u'item2', 0, 800).render(response.pubsub) + self.stub.send(response) + + return d + + def test_itemsIndex(self): + """ + Test sending items request to get a page out of order. + """ + def cb(response): + items, rsm = response + self.assertEquals(3, len(items)) + self.assertEquals([item1, item2, item3], items) + self.assertEquals(rsm, RSMResponse('item4', 'item6', 3, 800)) + + d = self.protocol.items(JID('pubsub.example.org'), 'test', + rsm_request=RSMRequest(3, index=3)) + d.addCallback(cb) + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('get', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(pubsub.NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'items', pubsub.NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + + set_elts = list(domish.generateElementsQNamed(iq.pubsub.children, + 'set', NS_RSM)) + self.assertEquals(1, len(set_elts)) + set_elt = set_elts[0] + self.assertEquals(u'3', ''.join(set_elt.max.children)) + self.assertEquals(u'3', ''.join(set_elt.index.children)) + + response = toResponse(iq, 'result') + items = response.addElement((pubsub.NS_PUBSUB, + 'pubsub')).addElement('items') + items['node'] = 'test' + item1 = items.addElement('item') + item1['id'] = 'item4' + item2 = items.addElement('item') + item2['id'] = 'item5' + item3 = items.addElement('item') + item3['id'] = 'item6' + RSMResponse(u'item4', u'item6', 3, 800).render(response.pubsub) + self.stub.send(response) + + return d + + def test_itemsCount(self): + """ + Test sending items request to count them. + """ + def cb(response): + items, rsm = response + self.assertEquals(0, len(items)) + self.assertEquals(rsm, RSMResponse(count=800)) + + d = self.protocol.items(JID('pubsub.example.org'), 'test', + rsm_request=RSMRequest(0)) + d.addCallback(cb) + + iq = self.stub.output[-1] + self.assertEquals('pubsub.example.org', iq.getAttribute('to')) + self.assertEquals('get', iq.getAttribute('type')) + self.assertEquals('pubsub', iq.pubsub.name) + self.assertEquals(pubsub.NS_PUBSUB, iq.pubsub.uri) + children = list(domish.generateElementsQNamed(iq.pubsub.children, + 'items', pubsub.NS_PUBSUB)) + self.assertEquals(1, len(children)) + child = children[0] + self.assertEquals('test', child['node']) + + set_elts = list(domish.generateElementsQNamed(iq.pubsub.children, + 'set', NS_RSM)) + self.assertEquals(1, len(set_elts)) + set_elt = set_elts[0] + self.assertEquals(u'0', ''.join(set_elt.max.children)) + + response = toResponse(iq, 'result') + response.addElement((pubsub.NS_PUBSUB, 'pubsub')) + RSMResponse(count=800).render(response.pubsub) + self.stub.send(response) + + return d + + +class PubSubServiceTest(unittest.TestCase, TestableRequestHandlerMixin): + + def setUp(self): + self.stub = XmlStreamStub() + self.resource = pubsub.PubSubResource() + self.service = PubSubService(self.resource) + self.service.send = self.stub.xmlstream.send + + def test_on_items(self): + """ + On a items request, return the first item for the given node. + """ + xml = """ + + + + + + 1 + + + """ + + def items(request): + rsm = RSMResponse(u'item', u'item', 0, 800).toElement() + return defer.succeed([pubsub.Item('current'), rsm]) + + def cb(element): + self.assertEqual(pubsub.NS_PUBSUB, element.uri) + self.assertEqual(pubsub.NS_PUBSUB, element.items.uri) + self.assertEqual(1, len(element.items.children)) + item = element.items.children[-1] + self.assertTrue(domish.IElement.providedBy(item)) + self.assertEqual('item', item.name) + self.assertEqual(pubsub.NS_PUBSUB, item.uri) + self.assertEqual('current', item['id']) + self.assertEqual(NS_RSM, element.set.uri) + self.assertEqual('800', ''.join(element.set.count.children)) + self.assertEqual('0', element.set.first['index']) + self.assertEqual('item', ''.join(element.set.first.children)) + self.assertEqual('item', ''.join(element.set.last.children)) + + self.resource.items = items + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + def test_on_itemsIndex(self): + """ + On a items request, return some items out of order for the given node. + """ + xml = """ + + + + + + 2 + 3 + + + """ + + def items(request): + rsm = RSMResponse(u'i1', u'i2', 3, 800).toElement() + return defer.succeed([pubsub.Item('i1'), pubsub.Item('i2'), rsm]) + + def cb(element): + self.assertEqual(pubsub.NS_PUBSUB, element.uri) + self.assertEqual(pubsub.NS_PUBSUB, element.items.uri) + self.assertEqual(2, len(element.items.children)) + item = element.items.children[0] + self.assertTrue(domish.IElement.providedBy(item)) + self.assertEqual('item', item.name) + self.assertEqual(pubsub.NS_PUBSUB, item.uri) + self.assertEqual('i1', item['id']) + item = element.items.children[1] + self.assertTrue(domish.IElement.providedBy(item)) + self.assertEqual('item', item.name) + self.assertEqual(pubsub.NS_PUBSUB, item.uri) + self.assertEqual('i2', item['id']) + self.assertEqual(NS_RSM, element.set.uri) + self.assertEqual('800', ''.join(element.set.count.children)) + self.assertEqual('3', element.set.first['index']) + self.assertEqual('i1', ''.join(element.set.first.children)) + self.assertEqual('i2', ''.join(element.set.last.children)) + + self.resource.items = items + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + def test_on_itemsCount(self): + """ + On a items request, return the items count. + """ + xml = """ + + + + + + 0 + + + """ + + def items(request): + rsm = RSMResponse(count=800).toElement() + return defer.succeed([rsm]) + + def cb(element): + self.assertEqual(pubsub.NS_PUBSUB, element.uri) + self.assertEqual(pubsub.NS_PUBSUB, element.items.uri) + self.assertEqual(0, len(element.items.children)) + self.assertEqual(NS_RSM, element.set.uri) + self.assertEqual('800', ''.join(element.set.count.children)) + self.assertEqual(None, element.set.first) + self.assertEqual(None, element.set.last) + + self.resource.items = items + verify.verifyObject(iwokkel.IPubSubResource, self.resource) + d = self.handleRequest(xml) + d.addCallback(cb) + return d diff -r 7430d1f6db22 -r c8cb4e867897 setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Thu Nov 02 22:50:59 2017 +0100 @@ -0,0 +1,54 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# SàT tmp: repository to store temporarily patches to third party software +# until they are merged upstream +# Copyright (C) 2017 Arnaud Joset (info@agayon.be) +# Copyright (C) 2009-2017 Jérôme Poisson (goffi@goffi.org) +# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import sys +from setuptools import setup, find_packages + + +includefiles = ['COPYING','README'] +include_modules = [] + +base = None +NAME = 'sat_tmp' +is_wheel = 'bdist_wheel' in sys.argv + +excluded = [] + +def create_package_list(base_package): + return ([base_package] + [base_package + '.' + pkg for pkg in find_packages(base_package)]) + +setup_info = dict( + name=NAME, + version='0.7', + author='Association « Salut à Toi »', + author_email='contact@salut-a-toi.org', + url='https://salut-a-toi.org', + classifiers=['Development Status :: 3 - Alpha', + 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', + 'Operating System :: POSIX :: Linux', + 'Topic :: Communications :: Chat'], + install_requires=['wokkel >= 0.7.1'], + packages=create_package_list('sat_tmp'), + zip_safe=True, +) + +setup(**setup_info) diff -r 7430d1f6db22 -r c8cb4e867897 wokkel/__init__.py diff -r 7430d1f6db22 -r c8cb4e867897 wokkel/mam.py --- a/wokkel/mam.py Wed Nov 01 22:34:51 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,620 +0,0 @@ -# -*- coding: utf-8 -*- -# -*- test-case-name: wokkel.test.test_mam -*- -# -# SàT Wokkel extension for Message Archive Management (XEP-0313) -# Copyright (C) 2015 Jérôme Poisson (goffi@goffi.org) -# Copyright (C) 2015 Adien Cossa (souliane@mailoo.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -""" -XMPP Message Archive Management protocol. - -This protocol is specified in -U{XEP-0313}. -""" - -from dateutil import tz - -from zope.interface import implements -from zope.interface import Interface - -from twisted.words.protocols.jabber import xmlstream -from twisted.words.xish import domish -from twisted.words.protocols.jabber import jid -from twisted.words.protocols.jabber import error -from twisted.internet import defer -from twisted.python import log - -from wokkel import subprotocols -from wokkel import disco -from wokkel import data_form -from wokkel import delay - -import rsm - -NS_MAM = 'urn:xmpp:mam:1' -NS_FORWARD = 'urn:xmpp:forward:0' - -FIELDS_REQUEST = "/iq[@type='get']/query[@xmlns='%s']" % NS_MAM -ARCHIVE_REQUEST = "/iq[@type='set']/query[@xmlns='%s']" % NS_MAM -PREFS_GET_REQUEST = "/iq[@type='get']/prefs[@xmlns='%s']" % NS_MAM -PREFS_SET_REQUEST = "/iq[@type='set']/prefs[@xmlns='%s']" % NS_MAM - -# TODO: add the tests! - - -class MAMError(error.StanzaError): - """ - MAM error. - """ - def __init__(self, text=None): - error.StanzaError.__init__(self, 'bad-request', text=text) - - -class Unsupported(MAMError): - def __init__(self, feature, text=None): - self.feature = feature - MAMError.__init__(self, 'feature-not-implemented', - 'unsupported', - feature, - text) - - def __str__(self): - message = MAMError.__str__(self) - message += ', feature %r' % self.feature - return message - - -class MAMRequest(object): - """ - A Message Archive Management request. - - @ivar form: Data Form specifing the filters. - @itype form: L{data_form.Form} - - @ivar rsm: RSM request instance. - @itype rsm: L{rsm.RSMRequest} - - @ivar node: pubsub node id if querying a pubsub node, else None. - @itype node: C{unicode} - - @ivar query_id: id to use to track the query - @itype query_id: C{unicode} - """ - # FIXME: should be based on generic.Stanza - - def __init__(self, form=None, rsm_=None, node=None, query_id=None, sender=None, recipient=None): - if form is not None: - assert form.formType == 'submit' - assert form.formNamespace == NS_MAM - self.form = form - self.rsm = rsm_ - self.node = node - self.query_id = query_id - self.sender = sender - self.recipient = recipient - - @classmethod - def fromElement(cls, iq): - """Parse the DOM representation of a MAM request. - - @param iq: element containing a MAM . - @type iq: L{Element} - - @return: MAMRequest instance. - @rtype: L{MAMRequest} - """ - sender = jid.JID(iq.getAttribute('from')) - recipient = jid.JID(iq.getAttribute('to')) - try: - query = iq.elements(NS_MAM, 'query').next() - except StopIteration: - raise MAMError("Can't find MAM in element") - form = data_form.findForm(query, NS_MAM) - try: - rsm_request = rsm.RSMRequest.fromElement(query) - except rsm.RSMNotFoundError: - rsm_request = None - node = query.getAttribute('node') - query_id = query.getAttribute('queryid') - return MAMRequest(form, rsm_request, node, query_id, sender, recipient) - - def toElement(self): - """ - Return the DOM representation of this RSM request. - - @rtype: L{Element} - """ - mam_elt = domish.Element((NS_MAM, 'query')) - if self.node is not None: - mam_elt['node'] = self.node - if self.query_id is not None: - mam_elt['queryid'] = self.query_id - if self.form is not None: - mam_elt.addChild(self.form.toElement()) - if self.rsm is not None: - mam_elt.addChild(self.rsm.toElement()) - - return mam_elt - - def render(self, parent): - """Embed the DOM representation of this MAM request in the given element. - - @param parent: parent IQ element. - @type parent: L{Element} - - @return: MAM request element. - @rtype: L{Element} - """ - assert parent.name == 'iq' - mam_elt = self.toElement() - parent.addChild(mam_elt) - return mam_elt - - -class MAMPrefs(object): - """ - A Message Archive Management request. - - @param default: A value in ('always', 'never', 'roster'). - @type : C{unicode} or C{None} - - @param always (list): A list of JID instances. - @type always: C{list} - - @param never (list): A list of JID instances. - @type never: C{list} - """ - - def __init__(self, default=None, always=None, never=None): - if default is not None: - # default must be defined in response, but can be empty in request (see http://xmpp.org/extensions/xep-0313.html#config) - assert default in ('always', 'never', 'roster') - self.default = default - if always is not None: - assert isinstance(always, list) - else: - always = [] - self.always = always - if never is not None: - assert isinstance(never, list) - else: - never = [] - self.never = never - - @classmethod - def fromElement(cls, prefs_elt): - """Parse the DOM representation of a MAM request. - - @param prefs_elt: MAM request element. - @type prefs_elt: L{Element} - - @return: MAMPrefs instance. - @rtype: L{MAMPrefs} - """ - if prefs_elt.uri != NS_MAM or prefs_elt.name != 'prefs': - raise MAMError('Element provided is not a MAM request') - try: - default = prefs_elt['default'] - except KeyError: - # FIXME: return proper error here - raise MAMError('Element provided is not a valid MAM request') - - prefs = {} - for attr in ('always', 'never'): - prefs[attr] = [] - try: - pref = prefs_elt.elements(NS_MAM, attr).next() - except StopIteration: - # FIXME: return proper error here - raise MAMError('Element provided is not a valid MAM request') - else: - for jid_s in pref.elements(NS_MAM, 'jid'): - prefs[attr].append(jid.JID(jid_s)) - return MAMPrefs(default, **prefs) - - def toElement(self): - """ - Return the DOM representation of this RSM request. - - @rtype: L{Element} - """ - mam_elt = domish.Element((NS_MAM, 'prefs')) - if self.default: - mam_elt['default'] = self.default - for attr in ('always', 'never'): - attr_elt = mam_elt.addElement(attr) - jids = getattr(self, attr) - for jid_ in jids: - attr_elt.addElement('jid', content=jid_.full()) - return mam_elt - - def render(self, parent): - """Embed the DOM representation of this MAM request in the given element. - - @param parent: parent IQ element. - @type parent: L{Element} - - @return: MAM request element. - @rtype: L{Element} - """ - assert parent.name == 'iq' - mam_elt = self.toElement() - parent.addChild(mam_elt) - return mam_elt - - -class MAMClient(subprotocols.XMPPHandler): - """ - MAM client. - - This handler implements the protocol for sending out MAM requests. - """ - - def queryArchive(self, mam_query, service=None, sender=None): - """Query a user, MUC or pubsub archive. - - @param mam_query: query to use - @type form: L{MAMRequest} - - @param service: Entity offering the MAM service (None for user server). - @type service: L{JID} - - @param sender: Optional sender address. - @type sender: L{JID} - - @return: A deferred that fires upon receiving a response. - @rtype: L{Deferred} - """ - iq = xmlstream.IQ(self.xmlstream, 'set') - mam_query.render(iq) - if sender is not None: - iq['from'] = unicode(sender) - return iq.send(to=service.full() if service else None) - - def queryFields(self, service=None, sender=None): - """Ask the server about supported fields. - - @param service: Entity offering the MAM service (None for user archives). - @type service: L{JID} - - @param sender: Optional sender address. - @type sender: L{JID} - - @return: data Form with the fields, or None if not found - @rtype: L{Deferred} - """ - # http://xmpp.org/extensions/xep-0313.html#query-form - iq = xmlstream.IQ(self.xmlstream, 'get') - MAMRequest().render(iq) - if sender is not None: - iq['from'] = unicode(sender) - d = iq.send(to=service.full() if service else None) - d.addCallback(lambda iq_result: iq_result.elements(NS_MAM, 'query').next()) - d.addCallback(data_form.findForm, NS_MAM) - return d - - def queryPrefs(self, service=None, sender=None): - """Retrieve the current user preferences. - - @param service: Entity offering the MAM service (None for user archives). - @type service: L{JID} - - @param sender: Optional sender address. - @type sender: L{JID} - - @return: A deferred that fires upon receiving a response. - @rtype: L{Deferred} - """ - # http://xmpp.org/extensions/xep-0313.html#prefs - iq = xmlstream.IQ(self.xmlstream, 'get') - MAMPrefs().render(iq) - if sender is not None: - iq['from'] = unicode(sender) - return iq.send(to=service.full() if service else None) - - def setPrefs(self, service=None, default='roster', always=None, never=None, sender=None): - """Set new user preferences. - - @param service: Entity offering the MAM service (None for user archives). - @type service: L{JID} - - @param default: A value in ('always', 'never', 'roster'). - @type : C{unicode} - - @param always (list): A list of JID instances. - @type always: C{list} - - @param never (list): A list of JID instances. - @type never: C{list} - - @param sender: Optional sender address. - @type sender: L{JID} - - @return: A deferred that fires upon receiving a response. - @rtype: L{Deferred} - """ - # http://xmpp.org/extensions/xep-0313.html#prefs - assert default is not None - iq = xmlstream.IQ(self.xmlstream, 'set') - MAMPrefs(default, always, never).render(iq) - if sender is not None: - iq['from'] = unicode(sender) - return iq.send(to=service.full() if service else None) - - -class IMAMResource(Interface): - - def onArchiveRequest(self, mam): - """ - - @param mam: The MAM request. - @type mam: L{MAMQueryReques} - - @return: The RSM answer. - @rtype: L{RSMResponse} - """ - - def onPrefsGetRequest(self, requestor): - """ - - @param requestor: JID of the requestor. - @type requestor: L{JID} - - @return: The current settings. - @rtype: L{wokkel.mam.MAMPrefs} - """ - - def onPrefsSetRequest(self, prefs, requestor): - """ - - @param prefs: The new settings to set. - @type prefs: L{wokkel.mam.MAMPrefs} - - @param requestor: JID of the requestor. - @type requestor: L{JID} - - @return: The new current settings. - @rtype: L{wokkel.mam.MAMPrefs} - """ - -class IMAMService(Interface): - """ - Interface for XMPP MAM service. - """ - - def addFilter(self, field): - """ - Add a new filter for querying MAM archive. - - @param field: data form field of the filter - @type field: L{Form} - """ - - -class MAMService(subprotocols.XMPPHandler, subprotocols.IQHandlerMixin): - """ - Protocol implementation for a MAM service. - - This handler waits for XMPP Ping requests and sends a response. - """ - implements(IMAMService, disco.IDisco) - - _request_class = MAMRequest - - iqHandlers = {FIELDS_REQUEST: '_onFieldsRequest', - ARCHIVE_REQUEST: '_onArchiveRequest', - PREFS_GET_REQUEST: '_onPrefsGetRequest', - PREFS_SET_REQUEST: '_onPrefsSetRequest' - } - - _legacyFilters = {'start': {'fieldType': 'text-single', - 'var': 'start', - 'label': 'Starting time', - 'desc': 'Starting time a the result period.', - }, - 'end': {'fieldType': 'text-single', - 'var': 'end', - 'label': 'Ending time', - 'desc': 'Ending time of the result period.', - }, - 'with': {'fieldType': 'jid-single', - 'var': 'with', - 'label': 'Entity', - 'desc': 'Entity against which to match message.', - }, - } - - def __init__(self, resource): - """ - @param resource: instance implementing IMAMResource - @type resource: L{object} - """ - self.resource = resource - self.extra_fields = {} - - def connectionInitialized(self): - """ - Called when the XML stream has been initialized. - - This sets up an observer for incoming ping requests. - """ - self.xmlstream.addObserver(FIELDS_REQUEST, self.handleRequest) - self.xmlstream.addObserver(ARCHIVE_REQUEST, self.handleRequest) - self.xmlstream.addObserver(PREFS_GET_REQUEST, self.handleRequest) - self.xmlstream.addObserver(PREFS_SET_REQUEST, self.handleRequest) - - def addFilter(self, field): - """ - Add a new filter for querying MAM archive. - - @param field: data form field of the filter - @type field: L{Form} - """ - self.extra_fields[field.var] = field - - def _onFieldsRequest(self, iq): - """ - Called when a fields request has been received. - - This immediately replies with a result response. - """ - iq.handled = True - query = domish.Element((NS_MAM, 'query')) - query.addChild(buildForm(extra_fields=self.extra_fields).toElement(), formType='form') - return query - - def _onArchiveRequest(self, iq): - """ - Called when a message archive request has been received. - - This replies with the list of archived message and the result - @return: A tuple with list of message data (id, element, data) and RSM element - @rtype: C{tuple} - """ - iq.handled = True - mam_ = self._request_class.fromElement(iq) - - # remove unsupported filters - unsupported_fields = [] - if mam_.form: - for key, field in mam_.form.fields.iteritems(): - if key not in self._legacyFilters and key not in self.extra_fields: - log.msg('Ignored unsupported MAM filter: %s' % field) - unsupported_fields.append(key) - for key in unsupported_fields: - del mam_.form.fields[key] - - def forwardMessage(id_, elt, date): - msg = domish.Element((None, 'message')) - msg['to'] = iq['from'] - result = msg.addElement((NS_MAM, 'result')) - if mam_.query_id is not None: - result['queryid'] = mam_.query_id - result['id'] = id_ - forward = result.addElement((NS_FORWARD, 'forwarded')) - forward.addChild(delay.Delay(date).toElement()) - forward.addChild(elt) - self.xmlstream.send(msg) - - def cb(result): - msg_data, rsm_elt = result - for data in msg_data: - forwardMessage(*data) - - fin_elt = domish.Element((NS_MAM, 'fin')) - - if rsm_elt is not None: - fin_elt.addChild(rsm_elt) - return fin_elt - - d = defer.maybeDeferred(self.resource.onArchiveRequest, mam_) - d.addCallback(cb) - return d - - def _onPrefsGetRequest(self, iq): - """ - Called when a prefs get request has been received. - - This immediately replies with a result response. - """ - iq.handled = True - requestor = jid.JID(iq['from']) - - def cb(prefs): - return prefs.toElement() - - d = self.resource.onPrefsGetRequest(requestor).addCallback(cb) - return d - - def _onPrefsSetRequest(self, iq): - """ - Called when a prefs get request has been received. - - This immediately replies with a result response. - """ - iq.handled = True - - prefs = MAMPrefs.fromElement(iq.prefs) - requestor = jid.JID(iq['from']) - - def cb(prefs): - return prefs.toElement() - - d = self.resource.onPrefsSetRequest(prefs, requestor).addCallback(cb) - return d - - def getDiscoInfo(self, requestor, target, nodeIdentifier=''): - if nodeIdentifier: - return [] - return [disco.DiscoFeature(NS_MAM)] - - def getDiscoItems(self, requestor, target, nodeIdentifier=''): - return [] - - -def datetime2utc(datetime_obj): - """Convert a datetime to a XEP-0082 compliant UTC datetime. - - @param datetime_obj: Offset-aware timestamp to convert. - @type datetime_obj: L{datetime} - - @return: The datetime converted to UTC. - @rtype: C{unicode} - """ - stampFormat = '%Y-%m-%dT%H:%M:%SZ' - return datetime_obj.astimezone(tz.tzutc()).strftime(stampFormat) - - -def buildForm(start=None, end=None, with_jid=None, extra_fields=None, formType='submit'): - """Prepare a Data Form for MAM. - - @param start: Offset-aware timestamp to filter out older messages. - @type start: L{datetime} - - @param end: Offset-aware timestamp to filter out later messages. - @type end: L{datetime} - - @param with_jid: JID against which to match messages. - @type with_jid: L{JID} - - @param extra_fields: list of extra data form fields that are not defined by the - specification. - @type: C{list} - - @param formType: The type of the Data Form ('submit' or 'form'). - @type formType: C{unicode} - - @return: XEP-0004 Data Form object. - @rtype: L{Form} - """ - form = data_form.Form(formType, formNamespace=NS_MAM) - - if formType == 'form': - for kwargs in MAMService._legacyFilters.values(): - form.addField(data_form.Field(**kwargs)) - elif formType == 'submit': - if start: - form.addField(data_form.Field(var='start', value=datetime2utc(start))) - if end: - form.addField(data_form.Field(var='end', value=datetime2utc(end))) - if with_jid: - form.addField(data_form.Field(fieldType='jid-single', var='with', value=with_jid.full())) - - if extra_fields is not None: - for field in extra_fields: - form.addField(field) - - return form diff -r 7430d1f6db22 -r c8cb4e867897 wokkel/pubsub.py --- a/wokkel/pubsub.py Wed Nov 01 22:34:51 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1717 +0,0 @@ -# -*- coding: utf-8 -*- -# -*- test-case-name: wokkel.test.test_pubsub -*- -# -# SàT adaptation for wokkel.pubsub -# Copyright (C) 2015 Adien Cossa (souliane@mailoo.org) -# Copyright (c) 2003-2012 Ralph Meijer. - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -- - -# This program is based on wokkel (https://wokkel.ik.nu/), -# originaly written by Ralph Meijer -# It is sublicensed under AGPL v3 (or any later version) as allowed by the original -# license. - -# -- - -# Here is a copy of the original license: - -# Copyright (c) 2003-2012 Ralph Meijer. -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -""" -XMPP publish-subscribe protocol. - -This protocol is specified in -U{XEP-0060}. -""" - -from zope.interface import implements - -from twisted.internet import defer -from twisted.python import log -from twisted.words.protocols.jabber import jid, error -from twisted.words.xish import domish - -from wokkel import disco, data_form, generic, shim -from wokkel.compat import IQ -from wokkel.subprotocols import IQHandlerMixin, XMPPHandler -from wokkel.iwokkel import IPubSubClient, IPubSubService, IPubSubResource - -# Iq get and set XPath queries -IQ_GET = '/iq[@type="get"]' -IQ_SET = '/iq[@type="set"]' - -# Publish-subscribe namespaces -NS_PUBSUB = 'http://jabber.org/protocol/pubsub' -NS_PUBSUB_EVENT = NS_PUBSUB + '#event' -NS_PUBSUB_ERRORS = NS_PUBSUB + '#errors' -NS_PUBSUB_OWNER = NS_PUBSUB + "#owner" -NS_PUBSUB_NODE_CONFIG = NS_PUBSUB + "#node_config" -NS_PUBSUB_META_DATA = NS_PUBSUB + "#meta-data" -NS_PUBSUB_SUBSCRIBE_OPTIONS = NS_PUBSUB + "#subscribe_options" - -# XPath to match pubsub requests -PUBSUB_REQUEST = '/iq[@type="get" or @type="set"]/' + \ - 'pubsub[@xmlns="' + NS_PUBSUB + '" or ' + \ - '@xmlns="' + NS_PUBSUB_OWNER + '"]' - -BOOL_TRUE = ('1','true') -BOOL_FALSE = ('0','false') - -class SubscriptionPending(Exception): - """ - Raised when the requested subscription is pending acceptance. - """ - - - -class SubscriptionUnconfigured(Exception): - """ - Raised when the requested subscription needs to be configured before - becoming active. - """ - - - -class PubSubError(error.StanzaError): - """ - Exception with publish-subscribe specific condition. - """ - def __init__(self, condition, pubsubCondition, feature=None, text=None): - appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition)) - if feature: - appCondition['feature'] = feature - error.StanzaError.__init__(self, condition, - text=text, - appCondition=appCondition) - - - -class BadRequest(error.StanzaError): - """ - Bad request stanza error. - """ - def __init__(self, pubsubCondition=None, text=None): - if pubsubCondition: - appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition)) - else: - appCondition = None - error.StanzaError.__init__(self, 'bad-request', - text=text, - appCondition=appCondition) - - - -class Unsupported(PubSubError): - def __init__(self, feature, text=None): - self.feature = feature - PubSubError.__init__(self, 'feature-not-implemented', - 'unsupported', - feature, - text) - - def __str__(self): - message = PubSubError.__str__(self) - message += ', feature %r' % self.feature - return message - - -class Subscription(object): - """ - A subscription to a node. - - @ivar nodeIdentifier: The identifier of the node subscribed to. The root - node is denoted by C{None}. - @type nodeIdentifier: C{unicode} - - @ivar subscriber: The subscribing entity. - @type subscriber: L{jid.JID} - - @ivar state: The subscription state. One of C{'subscribed'}, C{'pending'}, - C{'unconfigured'}, C{'none'}. - @type state: C{unicode} - - @ivar options: Optional list of subscription options. - @type options: C{dict} - - @ivar subscriptionIdentifier: Optional subscription identifier. - @type subscriptionIdentifier: C{unicode} - """ - - def __init__(self, nodeIdentifier, subscriber, state, options=None, - subscriptionIdentifier=None): - self.nodeIdentifier = nodeIdentifier - self.subscriber = subscriber - self.state = state - self.options = options or {} - self.subscriptionIdentifier = subscriptionIdentifier - - - @staticmethod - def fromElement(element): - return Subscription( - element.getAttribute('node'), - jid.JID(element.getAttribute('jid')), - element.getAttribute('subscription'), - subscriptionIdentifier=element.getAttribute('subid')) - - - def toElement(self, defaultUri=None): - """ - Return the DOM representation of this subscription. - - @rtype: L{domish.Element} - """ - element = domish.Element((defaultUri, 'subscription')) - if self.nodeIdentifier: - element['node'] = self.nodeIdentifier - element['jid'] = unicode(self.subscriber) - element['subscription'] = self.state - if self.subscriptionIdentifier: - element['subid'] = self.subscriptionIdentifier - return element - - - -class Item(domish.Element): - """ - Publish subscribe item. - - This behaves like an object providing L{domish.IElement}. - - Item payload can be added using C{addChild} or C{addRawXml}, or using the - C{payload} keyword argument to C{__init__}. - """ - - def __init__(self, id=None, payload=None): - """ - @param id: optional item identifier - @type id: C{unicode} - @param payload: optional item payload. Either as a domish element, or - as serialized XML. - @type payload: object providing L{domish.IElement} or C{unicode}. - """ - - domish.Element.__init__(self, (None, 'item')) - if id is not None: - self['id'] = id - if payload is not None: - if isinstance(payload, basestring): - self.addRawXml(payload) - else: - self.addChild(payload) - - - -class PubSubRequest(generic.Stanza): - """ - A publish-subscribe request. - - The set of instance variables used depends on the type of request. If - a variable is not applicable or not passed in the request, its value is - C{None}. - - @ivar verb: The type of publish-subscribe request. See C{_requestVerbMap}. - @type verb: C{str}. - - @ivar affiliations: Affiliations to be modified. - @type affiliations: C{set} - - @ivar items: The items to be published, as L{domish.Element}s. - @type items: C{list} - - @ivar itemIdentifiers: Identifiers of the items to be retrieved or - retracted. - @type itemIdentifiers: C{set} - - @ivar maxItems: Maximum number of items to retrieve. - @type maxItems: C{int}. - - @ivar nodeIdentifier: Identifier of the node the request is about. - @type nodeIdentifier: C{unicode} - - @ivar nodeType: The type of node that should be created, or for which the - configuration is retrieved. C{'leaf'} or C{'collection'}. - @type nodeType: C{str} - - @ivar options: Configurations options for nodes, subscriptions and publish - requests. - @type options: L{data_form.Form} - - @ivar subscriber: The subscribing entity. - @type subscriber: L{JID} - - @ivar subscriptionIdentifier: Identifier for a specific subscription. - @type subscriptionIdentifier: C{unicode} - - @ivar subscriptions: Subscriptions to be modified, as a set of - L{Subscription}. - @type subscriptions: C{set} - - @ivar affiliations: Affiliations to be modified, as a dictionary of entity - (L{JID} to affiliation - (C{unicode}). - @type affiliations: C{dict} - """ - - verb = None - - items = None - itemIdentifiers = None - maxItems = None - nodeIdentifier = None - nodeType = None - options = None - subscriber = None - subscriptionIdentifier = None - subscriptions = None - affiliations = None - notify = None - - # Map request iq type and subelement name to request verb - _requestVerbMap = { - ('set', NS_PUBSUB, 'publish'): 'publish', - ('set', NS_PUBSUB, 'subscribe'): 'subscribe', - ('set', NS_PUBSUB, 'unsubscribe'): 'unsubscribe', - ('get', NS_PUBSUB, 'options'): 'optionsGet', - ('set', NS_PUBSUB, 'options'): 'optionsSet', - ('get', NS_PUBSUB, 'subscriptions'): 'subscriptions', - ('get', NS_PUBSUB, 'affiliations'): 'affiliations', - ('set', NS_PUBSUB, 'create'): 'create', - ('get', NS_PUBSUB_OWNER, 'default'): 'default', - ('get', NS_PUBSUB_OWNER, 'configure'): 'configureGet', - ('set', NS_PUBSUB_OWNER, 'configure'): 'configureSet', - ('get', NS_PUBSUB, 'items'): 'items', - ('set', NS_PUBSUB, 'retract'): 'retract', - ('set', NS_PUBSUB_OWNER, 'purge'): 'purge', - ('set', NS_PUBSUB_OWNER, 'delete'): 'delete', - ('get', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsGet', - ('set', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsSet', - ('get', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsGet', - ('set', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsSet', - } - - # Map request verb to request iq type and subelement name - _verbRequestMap = dict(((v, k) for k, v in _requestVerbMap.iteritems())) - - # Map request verb to parameter handler names - _parameters = { - 'publish': ['node', 'items'], - 'subscribe': ['nodeOrEmpty', 'jid', 'optionsWithSubscribe'], - 'unsubscribe': ['nodeOrEmpty', 'jid', 'subidOrNone'], - 'optionsGet': ['nodeOrEmpty', 'jid', 'subidOrNone'], - 'optionsSet': ['nodeOrEmpty', 'jid', 'options', 'subidOrNone'], - 'subscriptions': ['nodeOrEmpty'], - 'affiliations': ['nodeOrNone'], - 'create': ['nodeOrNone', 'configureOrNone'], - 'default': ['default'], - 'configureGet': ['nodeOrEmpty'], - 'configureSet': ['nodeOrEmpty', 'configureOrNone'], - 'items': ['node', 'maxItems', 'itemIdentifiers', 'subidOrNone'], - 'retract': ['node', 'notify', 'itemIdentifiers'], - 'purge': ['node'], - 'delete': ['node'], - 'affiliationsGet': ['node'], - 'affiliationsSet': ['node', 'affiliations'], - 'subscriptionsGet': ['node'], - 'subscriptionsSet': ['node', 'subscriptions'], - } - - def __init__(self, verb=None): - self.verb = verb - - - def _parse_node(self, verbElement): - """ - Parse the required node identifier out of the verbElement. - """ - try: - self.nodeIdentifier = verbElement["node"] - except KeyError: - raise BadRequest('nodeid-required') - - - def _render_node(self, verbElement): - """ - Render the required node identifier on the verbElement. - """ - if not self.nodeIdentifier: - raise Exception("Node identifier is required") - - verbElement['node'] = self.nodeIdentifier - - - def _parse_nodeOrEmpty(self, verbElement): - """ - Parse the node identifier out of the verbElement. May be empty. - """ - self.nodeIdentifier = verbElement.getAttribute("node", '') - - - def _render_nodeOrEmpty(self, verbElement): - """ - Render the node identifier on the verbElement. May be empty. - """ - if self.nodeIdentifier: - verbElement['node'] = self.nodeIdentifier - - - def _parse_nodeOrNone(self, verbElement): - """ - Parse the optional node identifier out of the verbElement. - """ - self.nodeIdentifier = verbElement.getAttribute("node") - - - def _render_nodeOrNone(self, verbElement): - """ - Render the optional node identifier on the verbElement. - """ - if self.nodeIdentifier: - verbElement['node'] = self.nodeIdentifier - - - def _parse_items(self, verbElement): - """ - Parse items out of the verbElement for publish requests. - """ - self.items = [] - for element in verbElement.elements(): - if element.uri == NS_PUBSUB and element.name == 'item': - self.items.append(element) - - - def _render_items(self, verbElement): - """ - Render items into the verbElement for publish requests. - """ - if self.items: - for item in self.items: - item.uri = NS_PUBSUB - verbElement.addChild(item) - - - def _parse_jid(self, verbElement): - """ - Parse subscriber out of the verbElement for un-/subscribe requests. - """ - try: - self.subscriber = jid.internJID(verbElement["jid"]) - except KeyError: - raise BadRequest('jid-required') - - - def _render_jid(self, verbElement): - """ - Render subscriber into the verbElement for un-/subscribe requests. - """ - verbElement['jid'] = self.subscriber.full() - - - def _parse_default(self, verbElement): - """ - Parse node type out of a request for the default node configuration. - """ - form = data_form.findForm(verbElement, NS_PUBSUB_NODE_CONFIG) - if form is not None and form.formType == 'submit': - values = form.getValues() - self.nodeType = values.get('pubsub#node_type', 'leaf') - else: - self.nodeType = 'leaf' - - - def _parse_configure(self, verbElement): - """ - Parse options out of a request for setting the node configuration. - """ - form = data_form.findForm(verbElement, NS_PUBSUB_NODE_CONFIG) - if form is not None: - if form.formType in ('submit', 'cancel'): - self.options = form - else: - raise BadRequest(text=u"Unexpected form type '%s'" % form.formType) - else: - raise BadRequest(text="Missing configuration form") - - - def _parse_configureOrNone(self, verbElement): - """ - Parse optional node configuration form in create request. - """ - for element in verbElement.parent.elements(): - if element.uri in (NS_PUBSUB, NS_PUBSUB_OWNER) and element.name == 'configure': - form = data_form.findForm(element, NS_PUBSUB_NODE_CONFIG) - if form is not None: - if form.formType != 'submit': - raise BadRequest(text=u"Unexpected form type '%s'" % - form.formType) - else: - form = data_form.Form('submit', - formNamespace=NS_PUBSUB_NODE_CONFIG) - self.options = form - - - def _render_configureOrNone(self, verbElement): - """ - Render optional node configuration form in create request. - """ - if self.options is not None: - if verbElement.name == 'configure': - configure = verbElement - else: - configure = verbElement.parent.addElement('configure') - configure.addChild(self.options.toElement()) - - - def _parse_itemIdentifiers(self, verbElement): - """ - Parse item identifiers out of items and retract requests. - """ - self.itemIdentifiers = [] - for element in verbElement.elements(): - if element.uri == NS_PUBSUB and element.name == 'item': - try: - self.itemIdentifiers.append(element["id"]) - except KeyError: - raise BadRequest() - - - def _render_itemIdentifiers(self, verbElement): - """ - Render item identifiers into items and retract requests. - """ - if self.itemIdentifiers: - for itemIdentifier in self.itemIdentifiers: - item = verbElement.addElement('item') - item['id'] = itemIdentifier - - - def _parse_maxItems(self, verbElement): - """ - Parse maximum items out of an items request. - """ - value = verbElement.getAttribute('max_items') - - if value: - try: - self.maxItems = int(value) - except ValueError: - raise BadRequest(text="Field max_items requires a positive " + - "integer value") - - - def _render_maxItems(self, verbElement): - """ - Render maximum items into an items request. - """ - if self.maxItems: - verbElement['max_items'] = unicode(self.maxItems) - - - def _parse_subidOrNone(self, verbElement): - """ - Parse subscription identifier out of a request. - """ - self.subscriptionIdentifier = verbElement.getAttribute("subid") - - - def _render_subidOrNone(self, verbElement): - """ - Render subscription identifier into a request. - """ - if self.subscriptionIdentifier: - verbElement['subid'] = self.subscriptionIdentifier - - - def _parse_options(self, verbElement): - """ - Parse options form out of a subscription options request. - """ - form = data_form.findForm(verbElement, NS_PUBSUB_SUBSCRIBE_OPTIONS) - if form is not None: - if form.formType in ('submit', 'cancel'): - self.options = form - else: - raise BadRequest(text=u"Unexpected form type '%s'" % form.formType) - else: - raise BadRequest(text="Missing options form") - - - def _render_options(self, verbElement): - verbElement.addChild(self.options.toElement()) - - - def _parse_optionsWithSubscribe(self, verbElement): - for element in verbElement.parent.elements(): - if element.name == 'options' and element.uri == NS_PUBSUB: - form = data_form.findForm(element, - NS_PUBSUB_SUBSCRIBE_OPTIONS) - if form is not None: - if form.formType != 'submit': - raise BadRequest(text=u"Unexpected form type '%s'" % - form.formType) - else: - form = data_form.Form('submit', - formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) - self.options = form - - - def _render_optionsWithSubscribe(self, verbElement): - if self.options is not None: - optionsElement = verbElement.parent.addElement('options') - self._render_options(optionsElement) - - - def _parse_affiliations(self, verbElement): - self.affiliations = {} - for element in verbElement.elements(): - if (element.uri == NS_PUBSUB_OWNER and - element.name == 'affiliation'): - try: - entity = jid.internJID(element['jid']).userhostJID() - except KeyError: - raise BadRequest(text='Missing jid attribute') - - if entity in self.affiliations: - raise BadRequest(text='Multiple affiliations for an entity') - - try: - affiliation = element['affiliation'] - except KeyError: - raise BadRequest(text='Missing affiliation attribute') - - self.affiliations[entity] = affiliation - - - def _render_affiliations(self, verbElement): - for entity, affiliation in self.affiliations.iteritems(): - affiliationElement = verbElement.addElement((NS_PUBSUB_OWNER, 'affiliation')) - affiliationElement['jid'] = entity.full() - affiliationElement['affiliation'] = affiliation - - - def _parse_subscriptions(self, verbElement): - self.subscriptions = set() - seen_entities = set() - for element in verbElement.elements(): - if (element.uri == NS_PUBSUB_OWNER and - element.name == 'subscription'): - try: - subscriber = jid.internJID(element['jid']).userhostJID() - except KeyError: - raise BadRequest(text='Missing jid attribute') - - if subscriber in seen_entities: - raise BadRequest(text='Multiple subscriptions for an subscriber') - seen_entities.add(subscriber) - - try: - state = element['subscription'] - except KeyError: - # §8.8.2.1 says that value MUST NOT be changed - # if subscription is missing - continue - - self.subscriptions.add(Subscription(self.nodeIdentifier, - subscriber, - state)) - - - def _render_subscriptions(self, verbElement): - for subscription in self.subscriptions: - subscriptionElement = verbElement.addElement((NS_PUBSUB_OWNER, 'subscription')) - subscriptionElement['jid'] = subscription.subscriber.full() - subscriptionElement['subscription'] = subscription.state - - - def _parse_notify(self, verbElement): - value = verbElement.getAttribute('notify') - - if value: - if value in BOOL_TRUE: - self.notify = True - elif value in BOOL_FALSE: - self.notify = False - else: - raise BadRequest(text="Field notify must be a boolean value") - - - def _render_notify(self, verbElement): - if self.notify is not None: - verbElement['notify'] = "true" if self.notify else "false" - - - def parseElement(self, element): - """ - Parse the publish-subscribe verb and parameters out of a request. - """ - generic.Stanza.parseElement(self, element) - - verbs = [] - verbElements = [] - for child in element.pubsub.elements(): - key = (self.stanzaType, child.uri, child.name) - try: - verb = self._requestVerbMap[key] - except KeyError: - continue - - verbs.append(verb) - verbElements.append(child) - - if not verbs: - raise NotImplementedError() - - if len(verbs) > 1: - if 'optionsSet' in verbs and 'subscribe' in verbs: - self.verb = 'subscribe' - verbElement = verbElements[verbs.index('subscribe')] - else: - raise NotImplementedError() - else: - self.verb = verbs[0] - verbElement = verbElements[0] - - for parameter in self._parameters[self.verb]: - getattr(self, '_parse_%s' % parameter)(verbElement) - - - - def send(self, xs): - """ - Send this request to its recipient. - - This renders all of the relevant parameters for this specific - requests into an L{IQ}, and invoke its C{send} method. - This returns a deferred that fires upon reception of a response. See - L{IQ} for details. - - @param xs: The XML stream to send the request on. - @type xs: L{twisted.words.protocols.jabber.xmlstream.XmlStream} - @rtype: L{defer.Deferred}. - """ - - try: - (self.stanzaType, - childURI, - childName) = self._verbRequestMap[self.verb] - except KeyError: - raise NotImplementedError() - - iq = IQ(xs, self.stanzaType) - iq.addElement((childURI, 'pubsub')) - verbElement = iq.pubsub.addElement(childName) - - if self.sender: - iq['from'] = self.sender.full() - if self.recipient: - iq['to'] = self.recipient.full() - - for parameter in self._parameters[self.verb]: - getattr(self, '_render_%s' % parameter)(verbElement) - - return iq.send() - - - -class PubSubEvent(object): - """ - A publish subscribe event. - - @param sender: The entity from which the notification was received. - @type sender: L{jid.JID} - @param recipient: The entity to which the notification was sent. - @type recipient: L{wokkel.pubsub.ItemsEvent} - @param nodeIdentifier: Identifier of the node the event pertains to. - @type nodeIdentifier: C{unicode} - @param headers: SHIM headers, see L{wokkel.shim.extractHeaders}. - @type headers: C{dict} - """ - - def __init__(self, sender, recipient, nodeIdentifier, headers): - self.sender = sender - self.recipient = recipient - self.nodeIdentifier = nodeIdentifier - self.headers = headers - - - -class ItemsEvent(PubSubEvent): - """ - A publish-subscribe event that signifies new, updated and retracted items. - - @param items: List of received items as domish elements. - @type items: C{list} of L{domish.Element} - """ - - def __init__(self, sender, recipient, nodeIdentifier, items, headers): - PubSubEvent.__init__(self, sender, recipient, nodeIdentifier, headers) - self.items = items - - - -class DeleteEvent(PubSubEvent): - """ - A publish-subscribe event that signifies the deletion of a node. - """ - - redirectURI = None - - - -class PurgeEvent(PubSubEvent): - """ - A publish-subscribe event that signifies the purging of a node. - """ - - - -class PubSubClient(XMPPHandler): - """ - Publish subscribe client protocol. - """ - implements(IPubSubClient) - - _request_class = PubSubRequest - - def connectionInitialized(self): - self.xmlstream.addObserver('/message/event[@xmlns="%s"]' % - NS_PUBSUB_EVENT, self._onEvent) - - - def _onEvent(self, message): - if message.getAttribute('type') == 'error': - return - - try: - sender = jid.JID(message["from"]) - recipient = jid.JID(message["to"]) - except KeyError: - return - - actionElement = None - for element in message.event.elements(): - if element.uri == NS_PUBSUB_EVENT: - actionElement = element - - if not actionElement: - return - - eventHandler = getattr(self, "_onEvent_%s" % actionElement.name, None) - - if eventHandler: - headers = shim.extractHeaders(message) - eventHandler(sender, recipient, actionElement, headers) - message.handled = True - - - def _onEvent_items(self, sender, recipient, action, headers): - nodeIdentifier = action["node"] - - items = [element for element in action.elements() - if element.name in ('item', 'retract')] - - event = ItemsEvent(sender, recipient, nodeIdentifier, items, headers) - self.itemsReceived(event) - - - def _onEvent_delete(self, sender, recipient, action, headers): - nodeIdentifier = action["node"] - event = DeleteEvent(sender, recipient, nodeIdentifier, headers) - if action.redirect: - event.redirectURI = action.redirect.getAttribute('uri') - self.deleteReceived(event) - - - def _onEvent_purge(self, sender, recipient, action, headers): - nodeIdentifier = action["node"] - event = PurgeEvent(sender, recipient, nodeIdentifier, headers) - self.purgeReceived(event) - - - def itemsReceived(self, event): - pass - - - def deleteReceived(self, event): - pass - - - def purgeReceived(self, event): - pass - - - def createNode(self, service, nodeIdentifier=None, options=None, - sender=None): - """ - Create a publish subscribe node. - - @param service: The publish subscribe service to create the node at. - @type service: L{JID} - @param nodeIdentifier: Optional suggestion for the id of the node. - @type nodeIdentifier: C{unicode} - @param options: Optional node configuration options. - @type options: C{dict} - """ - request = self._request_class('create') - request.recipient = service - request.nodeIdentifier = nodeIdentifier - request.sender = sender - - if options: - form = data_form.Form(formType='submit', - formNamespace=NS_PUBSUB_NODE_CONFIG) - form.makeFields(options) - request.options = form - - def cb(iq): - try: - new_node = iq.pubsub.create["node"] - except AttributeError: - # the suggested node identifier was accepted - new_node = nodeIdentifier - return new_node - - d = request.send(self.xmlstream) - d.addCallback(cb) - return d - - - def deleteNode(self, service, nodeIdentifier, sender=None): - """ - Delete a publish subscribe node. - - @param service: The publish subscribe service to delete the node from. - @type service: L{JID} - @param nodeIdentifier: The identifier of the node. - @type nodeIdentifier: C{unicode} - """ - request = self._request_class('delete') - request.recipient = service - request.nodeIdentifier = nodeIdentifier - request.sender = sender - return request.send(self.xmlstream) - - - def subscribe(self, service, nodeIdentifier, subscriber, - options=None, sender=None): - """ - Subscribe to a publish subscribe node. - - @param service: The publish subscribe service that keeps the node. - @type service: L{JID} - - @param nodeIdentifier: The identifier of the node. - @type nodeIdentifier: C{unicode} - - @param subscriber: The entity to subscribe to the node. This entity - will get notifications of new published items. - @type subscriber: L{JID} - - @param options: Subscription options. - @type options: C{dict} - - @return: Deferred that fires with L{Subscription} or errbacks with - L{SubscriptionPending} or L{SubscriptionUnconfigured}. - @rtype: L{defer.Deferred} - """ - request = self._request_class('subscribe') - request.recipient = service - request.nodeIdentifier = nodeIdentifier - request.subscriber = subscriber - request.sender = sender - - if options: - form = data_form.Form(formType='submit', - formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) - form.makeFields(options) - request.options = form - - def cb(iq): - subscription = Subscription.fromElement(iq.pubsub.subscription) - - if subscription.state == 'pending': - raise SubscriptionPending() - elif subscription.state == 'unconfigured': - raise SubscriptionUnconfigured() - else: - # we assume subscription == 'subscribed' - # any other value would be invalid, but that should have - # yielded a stanza error. - return subscription - - d = request.send(self.xmlstream) - d.addCallback(cb) - return d - - - def unsubscribe(self, service, nodeIdentifier, subscriber, - subscriptionIdentifier=None, sender=None): - """ - Unsubscribe from a publish subscribe node. - - @param service: The publish subscribe service that keeps the node. - @type service: L{JID} - - @param nodeIdentifier: The identifier of the node. - @type nodeIdentifier: C{unicode} - - @param subscriber: The entity to unsubscribe from the node. - @type subscriber: L{JID} - - @param subscriptionIdentifier: Optional subscription identifier. - @type subscriptionIdentifier: C{unicode} - """ - request = self._request_class('unsubscribe') - request.recipient = service - request.nodeIdentifier = nodeIdentifier - request.subscriber = subscriber - request.subscriptionIdentifier = subscriptionIdentifier - request.sender = sender - return request.send(self.xmlstream) - - - def publish(self, service, nodeIdentifier, items=None, sender=None): - """ - Publish to a publish subscribe node. - - @param service: The publish subscribe service that keeps the node. - @type service: L{JID} - @param nodeIdentifier: The identifier of the node. - @type nodeIdentifier: C{unicode} - @param items: Optional list of L{Item}s to publish. - @type items: C{list} - """ - request = self._request_class('publish') - request.recipient = service - request.nodeIdentifier = nodeIdentifier - request.items = items - request.sender = sender - return request.send(self.xmlstream) - - - def items(self, service, nodeIdentifier, maxItems=None, itemIdentifiers=None, - subscriptionIdentifier=None, sender=None): - """ - Retrieve previously published items from a publish subscribe node. - - @param service: The publish subscribe service that keeps the node. - @type service: L{JID} - - @param nodeIdentifier: The identifier of the node. - @type nodeIdentifier: C{unicode} - - @param maxItems: Optional limit on the number of retrieved items. - @type maxItems: C{int} - - @param itemIdentifiers: Identifiers of the items to be retrieved. - @type itemIdentifiers: C{set} - - @param subscriptionIdentifier: Optional subscription identifier. In - case the node has been subscribed to multiple times, this narrows - the results to the specific subscription. - @type subscriptionIdentifier: C{unicode} - """ - request = self._request_class('items') - request.recipient = service - request.nodeIdentifier = nodeIdentifier - if maxItems: - request.maxItems = str(int(maxItems)) - request.subscriptionIdentifier = subscriptionIdentifier - request.sender = sender - request.itemIdentifiers = itemIdentifiers - - def cb(iq): - items = [] - for element in iq.pubsub.items.elements(): - if element.uri == NS_PUBSUB and element.name == 'item': - items.append(element) - return items - - d = request.send(self.xmlstream) - d.addCallback(cb) - return d - - def retractItems(self, service, nodeIdentifier, itemIdentifiers, notify=None, sender=None): - """ - Retract items from a publish subscribe node. - - @param service: The publish subscribe service to delete the node from. - @type service: L{JID} - @param nodeIdentifier: The identifier of the node. - @type nodeIdentifier: C{unicode} - @param itemIdentifiers: Identifiers of the items to be retracted. - @type itemIdentifiers: C{set} - @param notify: True if notification is required - @type notify: C{unicode} - """ - request = self._request_class('retract') - request.recipient = service - request.nodeIdentifier = nodeIdentifier - request.itemIdentifiers = itemIdentifiers - request.notify = notify - request.sender = sender - return request.send(self.xmlstream) - - def getOptions(self, service, nodeIdentifier, subscriber, - subscriptionIdentifier=None, sender=None): - """ - Get subscription options. - - @param service: The publish subscribe service that keeps the node. - @type service: L{JID} - - @param nodeIdentifier: The identifier of the node. - @type nodeIdentifier: C{unicode} - - @param subscriber: The entity subscribed to the node. - @type subscriber: L{JID} - - @param subscriptionIdentifier: Optional subscription identifier. - @type subscriptionIdentifier: C{unicode} - - @rtype: L{data_form.Form} - """ - request = self._request_class('optionsGet') - request.recipient = service - request.nodeIdentifier = nodeIdentifier - request.subscriber = subscriber - request.subscriptionIdentifier = subscriptionIdentifier - request.sender = sender - - def cb(iq): - form = data_form.findForm(iq.pubsub.options, - NS_PUBSUB_SUBSCRIBE_OPTIONS) - form.typeCheck() - return form - - d = request.send(self.xmlstream) - d.addCallback(cb) - return d - - - def setOptions(self, service, nodeIdentifier, subscriber, - options, subscriptionIdentifier=None, sender=None): - """ - Set subscription options. - - @param service: The publish subscribe service that keeps the node. - @type service: L{JID} - - @param nodeIdentifier: The identifier of the node. - @type nodeIdentifier: C{unicode} - - @param subscriber: The entity subscribed to the node. - @type subscriber: L{JID} - - @param options: Subscription options. - @type options: C{dict}. - - @param subscriptionIdentifier: Optional subscription identifier. - @type subscriptionIdentifier: C{unicode} - """ - request = self._request_class('optionsSet') - request.recipient = service - request.nodeIdentifier = nodeIdentifier - request.subscriber = subscriber - request.subscriptionIdentifier = subscriptionIdentifier - request.sender = sender - - form = data_form.Form(formType='submit', - formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) - form.makeFields(options) - request.options = form - - d = request.send(self.xmlstream) - return d - - - -class PubSubService(XMPPHandler, IQHandlerMixin): - """ - Protocol implementation for a XMPP Publish Subscribe Service. - - The word Service here is used as taken from the Publish Subscribe - specification. It is the party responsible for keeping nodes and their - subscriptions, and sending out notifications. - - Methods from the L{IPubSubService} interface that are called as a result - of an XMPP request may raise exceptions. Alternatively the deferred - returned by these methods may have their errback called. These are handled - as follows: - - - If the exception is an instance of L{error.StanzaError}, an error - response iq is returned. - - Any other exception is reported using L{log.msg}. An error response - with the condition C{internal-server-error} is returned. - - The default implementation of said methods raises an L{Unsupported} - exception and are meant to be overridden. - - @ivar discoIdentity: Service discovery identity as a dictionary with - keys C{'category'}, C{'type'} and C{'name'}. - @ivar pubSubFeatures: List of supported publish-subscribe features for - service discovery, as C{str}. - @type pubSubFeatures: C{list} or C{None} - """ - - implements(IPubSubService, disco.IDisco) - - iqHandlers = { - '/*': '_onPubSubRequest', - } - - _legacyHandlers = { - 'publish': ('publish', ['sender', 'recipient', - 'nodeIdentifier', 'items']), - 'subscribe': ('subscribe', ['sender', 'recipient', - 'nodeIdentifier', 'subscriber']), - 'unsubscribe': ('unsubscribe', ['sender', 'recipient', - 'nodeIdentifier', 'subscriber']), - 'subscriptions': ('subscriptions', ['sender', 'recipient']), - 'affiliations': ('affiliations', ['sender', 'recipient']), - 'create': ('create', ['sender', 'recipient', 'nodeIdentifier']), - 'getConfigurationOptions': ('getConfigurationOptions', []), - 'default': ('getDefaultConfiguration', - ['sender', 'recipient', 'nodeType']), - 'configureGet': ('getConfiguration', ['sender', 'recipient', - 'nodeIdentifier']), - 'configureSet': ('setConfiguration', ['sender', 'recipient', - 'nodeIdentifier', 'options']), - 'items': ('items', ['sender', 'recipient', 'nodeIdentifier', - 'maxItems', 'itemIdentifiers']), - 'retract': ('retract', ['sender', 'recipient', 'nodeIdentifier', - 'itemIdentifiers']), - 'purge': ('purge', ['sender', 'recipient', 'nodeIdentifier']), - 'delete': ('delete', ['sender', 'recipient', 'nodeIdentifier']), - } - - _request_class = PubSubRequest - - hideNodes = False - - def __init__(self, resource=None): - self.resource = resource - self.discoIdentity = {'category': 'pubsub', - 'type': 'service', - 'name': 'Generic Publish-Subscribe Service'} - - self.pubSubFeatures = [] - - - def connectionMade(self): - self.xmlstream.addObserver(PUBSUB_REQUEST, self.handleRequest) - - - def getDiscoInfo(self, requestor, target, nodeIdentifier=''): - def toInfo(nodeInfo): - if not nodeInfo: - return - - (nodeType, metaData) = nodeInfo['type'], nodeInfo['meta-data'] - info.append(disco.DiscoIdentity('pubsub', nodeType)) - if metaData: - form = data_form.Form(formType="result", - formNamespace=NS_PUBSUB_META_DATA) - form.addField( - data_form.Field( - var='pubsub#node_type', - value=nodeType, - label='The type of node (collection or leaf)' - ) - ) - - for metaDatum in metaData: - form.addField(data_form.Field.fromDict(metaDatum)) - - info.append(form) - - return - - info = [] - - request = self._request_class('discoInfo') - - if self.resource is not None: - resource = self.resource.locateResource(request) - identity = resource.discoIdentity - features = resource.features - getInfo = resource.getInfo - else: - category = self.discoIdentity['category'] - idType = self.discoIdentity['type'] - name = self.discoIdentity['name'] - identity = disco.DiscoIdentity(category, idType, name) - features = self.pubSubFeatures - getInfo = self.getNodeInfo - - if not nodeIdentifier: - info.append(identity) - info.append(disco.DiscoFeature(disco.NS_DISCO_ITEMS)) - info.extend([disco.DiscoFeature("%s#%s" % (NS_PUBSUB, feature)) - for feature in features]) - - d = defer.maybeDeferred(getInfo, requestor, target, nodeIdentifier or '') - d.addCallback(toInfo) - d.addErrback(log.err) - d.addCallback(lambda _: info) - return d - - - def _parseNodes(self, nodes, target): - """parse return values of resource.getNodes - - basestring values are used as node - tuple are unpacked as node, name - """ - items = [] - for node in nodes: - if isinstance(node, basestring): - items.append(disco.DiscoItem(target, node)) - else: - _node, name = node - items.append(disco.DiscoItem(target, _node, name)) - return items - - - def getDiscoItems(self, requestor, target, nodeIdentifier=''): - if self.hideNodes: - d = defer.succeed([]) - elif self.resource is not None: - request = self._request_class('discoInfo') - resource = self.resource.locateResource(request) - d = resource.getNodes(requestor, target, nodeIdentifier) - elif nodeIdentifier: - d = self.getNodes(requestor, target) - else: - d = defer.succeed([]) - - d.addCallback(self._parseNodes, target) - return d - - - def _onPubSubRequest(self, iq): - request = self._request_class.fromElement(iq) - if self.resource is not None: - resource = self.resource.locateResource(request) - else: - resource = self - - # Preprocess the request, knowing the handling resource - try: - preProcessor = getattr(self, '_preProcess_%s' % request.verb) - except AttributeError: - pass - else: - request = preProcessor(resource, request) - if request is None: - return defer.succeed(None) - - # Process the request itself, - if resource is not self: - try: - handler = getattr(resource, request.verb) - except AttributeError: - text = "Request verb: %s" % request.verb - return defer.fail(Unsupported('', text)) - - d = handler(request) - else: - try: - handlerName, argNames = self._legacyHandlers[request.verb] - except KeyError: - text = "Request verb: %s" % request.verb - return defer.fail(Unsupported('', text)) - - handler = getattr(self, handlerName) - args = [getattr(request, arg) for arg in argNames] - d = handler(*args) - - # If needed, translate the result into a response - try: - cb = getattr(self, '_toResponse_%s' % request.verb) - except AttributeError: - pass - else: - d.addCallback(cb, resource, request) - - return d - - - def _toResponse_subscribe(self, result, resource, request): - response = domish.Element((NS_PUBSUB, "pubsub")) - response.addChild(result.toElement(NS_PUBSUB)) - return response - - - def _toResponse_subscriptions(self, result, resource, request): - response = domish.Element((NS_PUBSUB, 'pubsub')) - subscriptions = response.addElement('subscriptions') - for subscription in result: - subscriptions.addChild(subscription.toElement(NS_PUBSUB)) - return response - - - def _toResponse_affiliations(self, result, resource, request): - response = domish.Element((NS_PUBSUB, 'pubsub')) - affiliations = response.addElement('affiliations') - - for nodeIdentifier, affiliation in result: - item = affiliations.addElement('affiliation') - item['node'] = nodeIdentifier - item['affiliation'] = affiliation - - return response - - - def _toResponse_create(self, result, resource, request): - if not request.nodeIdentifier or request.nodeIdentifier != result: - response = domish.Element((NS_PUBSUB, 'pubsub')) - create = response.addElement('create') - create['node'] = result - return response - else: - return None - - - def _formFromConfiguration(self, resource, values): - fieldDefs = resource.getConfigurationOptions() - form = data_form.Form(formType="form", - formNamespace=NS_PUBSUB_NODE_CONFIG) - form.makeFields(values, fieldDefs) - return form - - - def _checkConfiguration(self, resource, form): - fieldDefs = resource.getConfigurationOptions() - form.typeCheck(fieldDefs, filterUnknown=True) - - - def _preProcess_create(self, resource, request): - if request.options: - self._checkConfiguration(resource, request.options) - return request - - - def _preProcess_default(self, resource, request): - if request.nodeType not in ('leaf', 'collection'): - raise error.StanzaError('not-acceptable') - else: - return request - - - def _toResponse_default(self, options, resource, request): - response = domish.Element((NS_PUBSUB_OWNER, "pubsub")) - default = response.addElement("default") - form = self._formFromConfiguration(resource, options) - default.addChild(form.toElement()) - return response - - - def _toResponse_configureGet(self, options, resource, request): - response = domish.Element((NS_PUBSUB_OWNER, "pubsub")) - configure = response.addElement("configure") - form = self._formFromConfiguration(resource, options) - configure.addChild(form.toElement()) - - if request.nodeIdentifier: - configure["node"] = request.nodeIdentifier - - return response - - - def _preProcess_configureSet(self, resource, request): - if request.options.formType == 'cancel': - return None - else: - self._checkConfiguration(resource, request.options) - return request - - - def _toResponse_items(self, result, resource, request): - response = domish.Element((NS_PUBSUB, 'pubsub')) - items = response.addElement('items') - items["node"] = request.nodeIdentifier - - for item in result: - item.uri = NS_PUBSUB - items.addChild(item) - - return response - - - def _createNotification(self, eventType, service, nodeIdentifier, - subscriber, subscriptions=None): - headers = [] - - if subscriptions: - for subscription in subscriptions: - if nodeIdentifier != subscription.nodeIdentifier: - headers.append(('Collection', subscription.nodeIdentifier)) - - message = domish.Element((None, "message")) - message["from"] = service.full() - message["to"] = subscriber.full() - event = message.addElement((NS_PUBSUB_EVENT, "event")) - - element = event.addElement(eventType) - element["node"] = nodeIdentifier - - if headers: - message.addChild(shim.Headers(headers)) - - return message - - - def _toResponse_affiliationsGet(self, result, resource, request): - response = domish.Element((NS_PUBSUB_OWNER, 'pubsub')) - affiliations = response.addElement('affiliations') - - if request.nodeIdentifier: - affiliations['node'] = request.nodeIdentifier - - for entity, affiliation in result.iteritems(): - item = affiliations.addElement('affiliation') - item['jid'] = entity.full() - item['affiliation'] = affiliation - - return response - - - def _toResponse_subscriptionsGet(self, result, resource, request): - response = domish.Element((NS_PUBSUB, 'pubsub')) - subscriptions = response.addElement('subscriptions') - subscriptions['node'] = request.nodeIdentifier - for subscription in result: - subscription_element = subscription.toElement(NS_PUBSUB) - del subscription_element['node'] - subscriptions.addChild(subscription_element) - return response - - - # public methods - - def notifyPublish(self, service, nodeIdentifier, notifications): - for subscriber, subscriptions, items in notifications: - message = self._createNotification('items', service, - nodeIdentifier, subscriber, - subscriptions) - for item in items: - item.uri = NS_PUBSUB_EVENT - message.event.items.addChild(item) - self.send(message) - - - def notifyRetract(self, service, nodeIdentifier, notifications): - for subscriber, subscriptions, items in notifications: - message = self._createNotification('items', service, - nodeIdentifier, subscriber, - subscriptions) - for item in items: - retract = domish.Element((None, "retract")) - retract['id'] = item['id'] - message.event.items.addChild(retract) - self.send(message) - - - def notifyDelete(self, service, nodeIdentifier, subscribers, - redirectURI=None): - for subscriber in subscribers: - message = self._createNotification('delete', service, - nodeIdentifier, - subscriber) - if redirectURI: - redirect = message.event.delete.addElement('redirect') - redirect['uri'] = redirectURI - self.send(message) - - - def getNodeInfo(self, requestor, service, nodeIdentifier): - return None - - - def getNodes(self, requestor, service): - return [] - - - def publish(self, requestor, service, nodeIdentifier, items): - raise Unsupported('publish') - - - def subscribe(self, requestor, service, nodeIdentifier, subscriber): - raise Unsupported('subscribe') - - - def unsubscribe(self, requestor, service, nodeIdentifier, subscriber): - raise Unsupported('subscribe') - - - def subscriptions(self, requestor, service): - raise Unsupported('retrieve-subscriptions') - - - def affiliations(self, requestor, service): - raise Unsupported('retrieve-affiliations') - - - def create(self, requestor, service, nodeIdentifier): - raise Unsupported('create-nodes') - - - def getConfigurationOptions(self): - return {} - - - def getDefaultConfiguration(self, requestor, service, nodeType): - raise Unsupported('retrieve-default') - - - def getConfiguration(self, requestor, service, nodeIdentifier): - raise Unsupported('config-node') - - - def setConfiguration(self, requestor, service, nodeIdentifier, options): - raise Unsupported('config-node') - - - def items(self, requestor, service, nodeIdentifier, maxItems, - itemIdentifiers): - raise Unsupported('retrieve-items') - - - def retract(self, requestor, service, nodeIdentifier, itemIdentifiers): - raise Unsupported('retract-items') - - - def purge(self, requestor, service, nodeIdentifier): - raise Unsupported('purge-nodes') - - - def delete(self, requestor, service, nodeIdentifier): - raise Unsupported('delete-nodes') - - - -class PubSubResource(object): - - implements(IPubSubResource) - - features = [] - discoIdentity = disco.DiscoIdentity('pubsub', - 'service', - 'Publish-Subscribe Service') - - - def locateResource(self, request): - return self - - - def getInfo(self, requestor, service, nodeIdentifier): - return defer.succeed(None) - - - def getNodes(self, requestor, service, nodeIdentifier): - return defer.succeed([]) - - - def getConfigurationOptions(self): - return {} - - - def publish(self, request): - return defer.fail(Unsupported('publish')) - - - def subscribe(self, request): - return defer.fail(Unsupported('subscribe')) - - - def unsubscribe(self, request): - return defer.fail(Unsupported('subscribe')) - - - def subscriptions(self, request): - return defer.fail(Unsupported('retrieve-subscriptions')) - - - def affiliations(self, request): - return defer.fail(Unsupported('retrieve-affiliations')) - - - def create(self, request): - return defer.fail(Unsupported('create-nodes')) - - - def default(self, request): - return defer.fail(Unsupported('retrieve-default')) - - - def configureGet(self, request): - return defer.fail(Unsupported('config-node')) - - - def configureSet(self, request): - return defer.fail(Unsupported('config-node')) - - - def items(self, request): - return defer.fail(Unsupported('retrieve-items')) - - - def retract(self, request): - return defer.fail(Unsupported('retract-items')) - - - def purge(self, request): - return defer.fail(Unsupported('purge-nodes')) - - - def delete(self, request): - return defer.fail(Unsupported('delete-nodes')) - - - def affiliationsGet(self, request): - return defer.fail(Unsupported('retrieve-affiliations')) - - - def affiliationsSet(self, request): - return defer.fail(Unsupported('modify-affiliations')) - - - def subscriptionsGet(self, request): - return defer.fail(Unsupported('manage-subscriptions')) - - - def subscriptionsSet(self, request): - return defer.fail(Unsupported('manage-subscriptions')) diff -r 7430d1f6db22 -r c8cb4e867897 wokkel/rsm.py --- a/wokkel/rsm.py Wed Nov 01 22:34:51 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,425 +0,0 @@ -# -*- coding: utf-8 -*- -# -*- test-case-name: wokkel.test.test_rsm -*- -# -# SàT Wokkel extension for Result Set Management (XEP-0059) -# Copyright (C) 2015 Adien Cossa (souliane@mailoo.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -""" -XMPP Result Set Management protocol. - -This protocol is specified in -U{XEP-0059}. -""" - -from twisted.words.xish import domish -from twisted.words.protocols.jabber import error - -import pubsub -import copy - - -NS_RSM = 'http://jabber.org/protocol/rsm' - - -class RSMError(error.StanzaError): - """ - RSM error. - """ - def __init__(self, text=None): - error.StanzaError.__init__(self, 'bad-request', text=text) - - -class RSMNotFoundError(Exception): - """ - An expected RSM element has not been found. - """ - - -class RSMRequest(object): - """ - A Result Set Management request. - - @ivar max_: limit on the number of retrieved items. - @itype max_: C{int} or C{unicode} - - @ivar index: starting index of the requested page. - @itype index: C{int} or C{unicode} or C{None} - - @ivar after: ID of the element immediately preceding the page. - @itype after: C{unicode} - - @ivar before: ID of the element immediately following the page. - @itype before: C{unicode} - """ - - def __init__(self, max_=10, after=None, before=None, index=None): - self.max = int(max_) - - if index is not None: - assert after is None and before is None - index = int(index) - self.index = index - - if after is not None: - assert before is None - assert isinstance(after, basestring) - self.after = after - - if before is not None: - assert isinstance(before, basestring) - self.before = before - - def __str__(self): - return "RSM Request: max={0.max} after={0.after} before={0.before} index={0.index}".format(self) - - @classmethod - def fromElement(cls, element): - """Parse the given request element. - - @param element: request containing a set element, or set element itself. - @type element: L{domish.Element} - - @return: RSMRequest instance. - @rtype: L{RSMRequest} - """ - - if element.name == 'set' and element.uri == NS_RSM: - set_elt = element - else: - try: - set_elt = element.elements(NS_RSM, 'set').next() - except StopIteration: - raise RSMNotFoundError() - - try: - before_elt = set_elt.elements(NS_RSM, 'before').next() - except StopIteration: - before = None - else: - before = unicode(before_elt) - - try: - after_elt = set_elt.elements(NS_RSM, 'after').next() - except StopIteration: - after = None - else: - after = unicode(after_elt) - if not after: - raise RSMError(" element can't be empty in RSM request") - - try: - max_elt = set_elt.elements(NS_RSM, 'max').next() - except StopIteration: - # FIXME: even if it doesn't make a lot of sense without it - # element is not mandatory in XEP-0059 - raise RSMError("RSM request is missing its 'max' element") - else: - try: - max_ = int(unicode(max_elt)) - except ValueError: - raise RSMError("bad value for 'max' element") - - try: - index_elt = set_elt.elements(NS_RSM, 'index').next() - except StopIteration: - index = None - else: - try: - index = int(unicode(index_elt)) - except ValueError: - raise RSMError("bad value for 'index' element") - - return RSMRequest(max_, after, before, index) - - def toElement(self): - """ - Return the DOM representation of this RSM request. - - @rtype: L{domish.Element} - """ - set_elt = domish.Element((NS_RSM, 'set')) - set_elt.addElement('max', content=unicode(self.max)) - - if self.index is not None: - set_elt.addElement('index', content=unicode(self.index)) - - if self.before is not None: - if self.before == '': # request the last page - set_elt.addElement('before') - else: - set_elt.addElement('before', content=self.before) - - if self.after is not None: - set_elt.addElement('after', content=self.after) - - return set_elt - - def render(self, element): - """Embed the DOM representation of this RSM request in the given element. - - @param element: Element to contain the RSM request. - @type element: L{domish.Element} - - @return: RSM request element. - @rtype: L{domish.Element} - """ - set_elt = self.toElement() - element.addChild(set_elt) - - return set_elt - - -class RSMResponse(object): - """ - A Result Set Management response. - - @ivar first: ID of the first element of the returned page. - @itype first: C{unicode} - - @ivar last: ID of the last element of the returned page. - @itype last: C{unicode} - - @ivar index: starting index of the returned page. - @itype index: C{int} - - @ivar count: total number of items. - @itype count: C{int} - - """ - - def __init__(self, first=None, last=None, index=None, count=None): - if first is None: - assert last is None and index is None - if last is None: - assert first is None - self.first = first - self.last = last - if count is not None: - self.count = int(count) - else: - self.count = None - if index is not None: - self.index = int(index) - else: - self.index = None - - def __str__(self): - return "RSM Request: first={0.first} last={0.last} index={0.index} count={0.count}".format(self) - - @classmethod - def fromElement(cls, element): - """Parse the given response element. - - @param element: response element. - @type element: L{domish.Element} - - @return: RSMResponse instance. - @rtype: L{RSMResponse} - """ - try: - set_elt = element.elements(NS_RSM, 'set').next() - except StopIteration: - raise RSMNotFoundError() - - try: - first_elt = set_elt.elements(NS_RSM, 'first').next() - except StopIteration: - first = None - index = None - else: - first = unicode(first_elt) - try: - index = int(first_elt['index']) - except KeyError: - index = None - except ValueError: - raise RSMError("bad index in RSM response") - - try: - last_elt = set_elt.elements(NS_RSM, 'last').next() - except StopIteration: - if first is not None: - raise RSMError("RSM response is missing its 'last' element") - else: - last = None - else: - if first is None: - raise RSMError("RSM response is missing its 'first' element") - last = unicode(last_elt) - - try: - count_elt = set_elt.elements(NS_RSM, 'count').next() - except StopIteration: - count = None - else: - try: - count = int(unicode(count_elt)) - except ValueError: - raise RSMError("invalid count in RSM response") - - return RSMResponse(first, last, index, count) - - def toElement(self): - """ - Return the DOM representation of this RSM request. - - @rtype: L{domish.Element} - """ - set_elt = domish.Element((NS_RSM, 'set')) - if self.first is not None: - first_elt = set_elt.addElement('first', content=self.first) - if self.index is not None: - first_elt['index'] = unicode(self.index) - - set_elt.addElement('last', content=self.last) - - if self.count is not None: - set_elt.addElement('count', content=unicode(self.count)) - - return set_elt - - def render(self, element): - """Embed the DOM representation of this RSM response in the given element. - - @param element: Element to contain the RSM response. - @type element: L{domish.Element} - - @return: RSM request element. - @rtype: L{domish.Element} - """ - set_elt = self.toElement() - element.addChild(set_elt) - return set_elt - - def toDict(self): - """Return a dict representation of the object. - - @return: a dict of strings. - @rtype: C{dict} binding C{unicode} to C{unicode} - """ - result = {} - for attr in ('first', 'last', 'index', 'count'): - value = getattr(self, attr) - if value is not None: - result[attr] = unicode(value) - return result - - -class PubSubRequest(pubsub.PubSubRequest): - """PubSubRequest extension to handle RSM. - - @ivar rsm: RSM request instance. - @type rsm: L{RSMRequest} - """ - - rsm = None - _parameters = copy.deepcopy(pubsub.PubSubRequest._parameters) - _parameters['items'].append('rsm') - - def _parse_rsm(self, verbElement): - try: - self.rsm = RSMRequest.fromElement(verbElement.parent) - except RSMNotFoundError: - self.rsm = None - - def _render_rsm(self, verbElement): - if self.rsm: - self.rsm.render(verbElement.parent) - - -class PubSubClient(pubsub.PubSubClient): - """PubSubClient extension to handle RSM.""" - - _request_class = PubSubRequest - - def items(self, service, nodeIdentifier, maxItems=None, itemIdentifiers=None, - subscriptionIdentifier=None, sender=None, rsm_request=None): - """ - Retrieve previously published items from a publish subscribe node. - - @param service: The publish subscribe service that keeps the node. - @type service: L{JID} - - @param nodeIdentifier: The identifier of the node. - @type nodeIdentifier: C{unicode} - - @param maxItems: Optional limit on the number of retrieved items. - @type maxItems: C{int} - - @param itemIdentifiers: Identifiers of the items to be retrieved. - @type itemIdentifiers: C{set} - - @param subscriptionIdentifier: Optional subscription identifier. In - case the node has been subscribed to multiple times, this narrows - the results to the specific subscription. - @type subscriptionIdentifier: C{unicode} - - @param ext_data: extension data. - @type ext_data: L{dict} - - @return: a Deferred that fires a C{list} of C{tuple} of L{domish.Element}, L{RSMResponse}. - @rtype: L{defer.Deferred} - """ - # XXX: we have to copy initial method instead of calling it, - # as original cb remove all non item elements - request = self._request_class('items') - request.recipient = service - request.nodeIdentifier = nodeIdentifier - if maxItems: - request.maxItems = str(int(maxItems)) - request.subscriptionIdentifier = subscriptionIdentifier - request.sender = sender - request.itemIdentifiers = itemIdentifiers - request.rsm = rsm_request - - def cb(iq): - items = [] - pubsub_elt = iq.pubsub - if pubsub_elt.items: - for element in pubsub_elt.items.elements(pubsub.NS_PUBSUB, 'item'): - items.append(element) - - try: - rsm_response = RSMResponse.fromElement(pubsub_elt) - except RSMNotFoundError: - rsm_response = None - return (items, rsm_response) - - d = request.send(self.xmlstream) - d.addCallback(cb) - return d - - -class PubSubService(pubsub.PubSubService): - """PubSubService extension to handle RSM.""" - - _request_class = PubSubRequest - - def _toResponse_items(self, elts, resource, request): - # default method only manage elements - # but we need to add RSM set element - rsm_elt = None - for idx, elt in enumerate(reversed(elts)): - if elt.name == "set" and elt.uri == NS_RSM: - rsm_elt = elts.pop(-1-idx) - break - - response = pubsub.PubSubService._toResponse_items(self, elts, - resource, request) - if rsm_elt is not None: - response.addChild(rsm_elt) - - return response diff -r 7430d1f6db22 -r c8cb4e867897 wokkel/test/__init__.py diff -r 7430d1f6db22 -r c8cb4e867897 wokkel/test/test_pubsub.py --- a/wokkel/test/test_pubsub.py Wed Nov 01 22:34:51 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4218 +0,0 @@ -# Copyright (c) Ralph Meijer. -# See LICENSE for details. - -""" -Tests for L{wokkel.pubsub} -""" - -from zope.interface import verify - -from twisted.trial import unittest -from twisted.internet import defer -from twisted.words.xish import domish -from twisted.words.protocols.jabber import error -from twisted.words.protocols.jabber.jid import JID -from twisted.words.protocols.jabber.xmlstream import toResponse - -from wokkel import data_form, disco, iwokkel, shim -from wokkel.generic import parseXml -from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub - -from sat.tmp.wokkel import pubsub - -NS_PUBSUB = 'http://jabber.org/protocol/pubsub' -NS_PUBSUB_NODE_CONFIG = 'http://jabber.org/protocol/pubsub#node_config' -NS_PUBSUB_ERRORS = 'http://jabber.org/protocol/pubsub#errors' -NS_PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event' -NS_PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner' -NS_PUBSUB_META_DATA = 'http://jabber.org/protocol/pubsub#meta-data' -NS_PUBSUB_SUBSCRIBE_OPTIONS = 'http://jabber.org/protocol/pubsub#subscribe_options' - -def calledAsync(fn): - """ - Function wrapper that fires a deferred upon calling the given function. - """ - d = defer.Deferred() - - def func(*args, **kwargs): - try: - result = fn(*args, **kwargs) - except: - d.errback() - else: - d.callback(result) - - return d, func - - -class SubscriptionTest(unittest.TestCase): - """ - Tests for L{pubsub.Subscription}. - """ - - def test_fromElement(self): - """ - fromElement parses a subscription from XML DOM. - """ - xml = """ - - """ - subscription = pubsub.Subscription.fromElement(parseXml(xml)) - self.assertEqual('test', subscription.nodeIdentifier) - self.assertEqual(JID('user@example.org/Home'), subscription.subscriber) - self.assertEqual('pending', subscription.state) - self.assertIdentical(None, subscription.subscriptionIdentifier) - - - def test_fromElementWithSubscriptionIdentifier(self): - """ - A subscription identifier in the subscription should be parsed, too. - """ - xml = """ - - """ - subscription = pubsub.Subscription.fromElement(parseXml(xml)) - self.assertEqual('1234', subscription.subscriptionIdentifier) - - - def test_toElement(self): - """ - Rendering a Subscription should yield the proper attributes. - """ - subscription = pubsub.Subscription('test', - JID('user@example.org/Home'), - 'pending') - element = subscription.toElement() - self.assertEqual('subscription', element.name) - self.assertEqual(None, element.uri) - self.assertEqual('test', element.getAttribute('node')) - self.assertEqual('user@example.org/Home', element.getAttribute('jid')) - self.assertEqual('pending', element.getAttribute('subscription')) - self.assertFalse(element.hasAttribute('subid')) - - - def test_toElementEmptyNodeIdentifier(self): - """ - The empty node identifier should not yield a node attribute. - """ - subscription = pubsub.Subscription('', - JID('user@example.org/Home'), - 'pending') - element = subscription.toElement() - self.assertFalse(element.hasAttribute('node')) - - - def test_toElementWithSubscriptionIdentifier(self): - """ - The subscription identifier, if set, is in the subid attribute. - """ - subscription = pubsub.Subscription('test', - JID('user@example.org/Home'), - 'pending', - subscriptionIdentifier='1234') - element = subscription.toElement() - self.assertEqual('1234', element.getAttribute('subid')) - - - -class PubSubClientTest(unittest.TestCase): - timeout = 2 - - def setUp(self): - self.stub = XmlStreamStub() - self.protocol = pubsub.PubSubClient() - self.protocol.xmlstream = self.stub.xmlstream - self.protocol.connectionInitialized() - - - def test_interface(self): - """ - Do instances of L{pubsub.PubSubClient} provide L{iwokkel.IPubSubClient}? - """ - verify.verifyObject(iwokkel.IPubSubClient, self.protocol) - - - def test_eventItems(self): - """ - Test receiving an items event resulting in a call to itemsReceived. - """ - message = domish.Element((None, 'message')) - message['from'] = 'pubsub.example.org' - message['to'] = 'user@example.org/home' - event = message.addElement((NS_PUBSUB_EVENT, 'event')) - items = event.addElement('items') - items['node'] = 'test' - item1 = items.addElement('item') - item1['id'] = 'item1' - item2 = items.addElement('retract') - item2['id'] = 'item2' - item3 = items.addElement('item') - item3['id'] = 'item3' - - def itemsReceived(event): - self.assertEquals(JID('user@example.org/home'), event.recipient) - self.assertEquals(JID('pubsub.example.org'), event.sender) - self.assertEquals('test', event.nodeIdentifier) - self.assertEquals([item1, item2, item3], event.items) - - d, self.protocol.itemsReceived = calledAsync(itemsReceived) - self.stub.send(message) - return d - - - def test_eventItemsCollection(self): - """ - Test receiving an items event resulting in a call to itemsReceived. - """ - message = domish.Element((None, 'message')) - message['from'] = 'pubsub.example.org' - message['to'] = 'user@example.org/home' - event = message.addElement((NS_PUBSUB_EVENT, 'event')) - items = event.addElement('items') - items['node'] = 'test' - - headers = shim.Headers([('Collection', 'collection')]) - message.addChild(headers) - - def itemsReceived(event): - self.assertEquals(JID('user@example.org/home'), event.recipient) - self.assertEquals(JID('pubsub.example.org'), event.sender) - self.assertEquals('test', event.nodeIdentifier) - self.assertEquals({'Collection': ['collection']}, event.headers) - - d, self.protocol.itemsReceived = calledAsync(itemsReceived) - self.stub.send(message) - return d - - - def test_eventItemsError(self): - """ - An error message with embedded event should not be handled. - - This test uses an items event, which should not result in itemsReceived - being called. In general message.handled should be False. - """ - message = domish.Element((None, 'message')) - message['from'] = 'pubsub.example.org' - message['to'] = 'user@example.org/home' - message['type'] = 'error' - event = message.addElement((NS_PUBSUB_EVENT, 'event')) - items = event.addElement('items') - items['node'] = 'test' - - class UnexpectedCall(Exception): - pass - - def itemsReceived(event): - raise UnexpectedCall("Unexpected call to itemsReceived") - - self.protocol.itemsReceived = itemsReceived - self.stub.send(message) - self.assertFalse(message.handled) - - - def test_eventDelete(self): - """ - Test receiving a delete event resulting in a call to deleteReceived. - """ - message = domish.Element((None, 'message')) - message['from'] = 'pubsub.example.org' - message['to'] = 'user@example.org/home' - event = message.addElement((NS_PUBSUB_EVENT, 'event')) - delete = event.addElement('delete') - delete['node'] = 'test' - - def deleteReceived(event): - self.assertEquals(JID('user@example.org/home'), event.recipient) - self.assertEquals(JID('pubsub.example.org'), event.sender) - self.assertEquals('test', event.nodeIdentifier) - - d, self.protocol.deleteReceived = calledAsync(deleteReceived) - self.stub.send(message) - return d - - - def test_eventDeleteRedirect(self): - """ - Test receiving a delete event with a redirect URI. - """ - message = domish.Element((None, 'message')) - message['from'] = 'pubsub.example.org' - message['to'] = 'user@example.org/home' - event = message.addElement((NS_PUBSUB_EVENT, 'event')) - delete = event.addElement('delete') - delete['node'] = 'test' - uri = 'xmpp:pubsub.example.org?;node=test2' - delete.addElement('redirect')['uri'] = uri - - def deleteReceived(event): - self.assertEquals(JID('user@example.org/home'), event.recipient) - self.assertEquals(JID('pubsub.example.org'), event.sender) - self.assertEquals('test', event.nodeIdentifier) - self.assertEquals(uri, event.redirectURI) - - d, self.protocol.deleteReceived = calledAsync(deleteReceived) - self.stub.send(message) - return d - - - def test_event_purge(self): - """ - Test receiving a purge event resulting in a call to purgeReceived. - """ - message = domish.Element((None, 'message')) - message['from'] = 'pubsub.example.org' - message['to'] = 'user@example.org/home' - event = message.addElement((NS_PUBSUB_EVENT, 'event')) - items = event.addElement('purge') - items['node'] = 'test' - - def purgeReceived(event): - self.assertEquals(JID('user@example.org/home'), event.recipient) - self.assertEquals(JID('pubsub.example.org'), event.sender) - self.assertEquals('test', event.nodeIdentifier) - - d, self.protocol.purgeReceived = calledAsync(purgeReceived) - self.stub.send(message) - return d - - - def test_createNode(self): - """ - Test sending create request. - """ - - def cb(nodeIdentifier): - self.assertEquals('test', nodeIdentifier) - - d = self.protocol.createNode(JID('pubsub.example.org'), 'test') - d.addCallback(cb) - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('set', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'create', NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - - response = toResponse(iq, 'result') - self.stub.send(response) - return d - - - def test_createNodeInstant(self): - """ - Test sending create request resulting in an instant node. - """ - - def cb(nodeIdentifier): - self.assertEquals('test', nodeIdentifier) - - d = self.protocol.createNode(JID('pubsub.example.org')) - d.addCallback(cb) - - iq = self.stub.output[-1] - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'create', NS_PUBSUB)) - child = children[0] - self.assertFalse(child.hasAttribute('node')) - - response = toResponse(iq, 'result') - command = response.addElement((NS_PUBSUB, 'pubsub')) - create = command.addElement('create') - create['node'] = 'test' - self.stub.send(response) - return d - - - def test_createNodeRenamed(self): - """ - Test sending create request resulting in renamed node. - """ - - def cb(nodeIdentifier): - self.assertEquals('test2', nodeIdentifier) - - d = self.protocol.createNode(JID('pubsub.example.org'), 'test') - d.addCallback(cb) - - iq = self.stub.output[-1] - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'create', NS_PUBSUB)) - child = children[0] - self.assertEquals('test', child['node']) - - response = toResponse(iq, 'result') - command = response.addElement((NS_PUBSUB, 'pubsub')) - create = command.addElement('create') - create['node'] = 'test2' - self.stub.send(response) - return d - - - def test_createNodeWithSender(self): - """ - Test sending create request from a specific JID. - """ - - d = self.protocol.createNode(JID('pubsub.example.org'), 'test', - sender=JID('user@example.org')) - - iq = self.stub.output[-1] - self.assertEquals('user@example.org', iq['from']) - - response = toResponse(iq, 'result') - self.stub.send(response) - return d - - - def test_createNodeWithConfig(self): - """ - Test sending create request with configuration options - """ - - options = { - 'pubsub#title': 'Princely Musings (Atom)', - 'pubsub#deliver_payloads': True, - 'pubsub#persist_items': '1', - 'pubsub#max_items': '10', - 'pubsub#access_model': 'open', - 'pubsub#type': 'http://www.w3.org/2005/Atom', - } - - d = self.protocol.createNode(JID('pubsub.example.org'), 'test', - sender=JID('user@example.org'), - options=options) - - iq = self.stub.output[-1] - - # check if there is exactly one configure element - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'configure', NS_PUBSUB)) - self.assertEqual(1, len(children)) - - # check that it has a configuration form - form = data_form.findForm(children[0], NS_PUBSUB_NODE_CONFIG) - self.assertEqual('submit', form.formType) - - - response = toResponse(iq, 'result') - self.stub.send(response) - return d - - - def test_deleteNode(self): - """ - Test sending delete request. - """ - - d = self.protocol.deleteNode(JID('pubsub.example.org'), 'test') - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('set', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(NS_PUBSUB_OWNER, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'delete', NS_PUBSUB_OWNER)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - - response = toResponse(iq, 'result') - self.stub.send(response) - return d - - - def test_deleteNodeWithSender(self): - """ - Test sending delete request. - """ - - d = self.protocol.deleteNode(JID('pubsub.example.org'), 'test', - sender=JID('user@example.org')) - - iq = self.stub.output[-1] - self.assertEquals('user@example.org', iq['from']) - - response = toResponse(iq, 'result') - self.stub.send(response) - return d - - - def test_publish(self): - """ - Test sending publish request. - """ - - item = pubsub.Item() - d = self.protocol.publish(JID('pubsub.example.org'), 'test', [item]) - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('set', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'publish', NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - items = list(domish.generateElementsQNamed(child.children, - 'item', NS_PUBSUB)) - self.assertEquals(1, len(items)) - self.assertIdentical(item, items[0]) - - response = toResponse(iq, 'result') - self.stub.send(response) - return d - - - def test_publishNoItems(self): - """ - Test sending publish request without items. - """ - - d = self.protocol.publish(JID('pubsub.example.org'), 'test') - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('set', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'publish', NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - - response = toResponse(iq, 'result') - self.stub.send(response) - return d - - - def test_publishWithSender(self): - """ - Test sending publish request from a specific JID. - """ - - item = pubsub.Item() - d = self.protocol.publish(JID('pubsub.example.org'), 'test', [item], - JID('user@example.org')) - - iq = self.stub.output[-1] - self.assertEquals('user@example.org', iq['from']) - - response = toResponse(iq, 'result') - self.stub.send(response) - return d - - - def test_subscribe(self): - """ - Test sending subscription request. - """ - d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', - JID('user@example.org')) - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('set', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'subscribe', NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - self.assertEquals('user@example.org', child['jid']) - - response = toResponse(iq, 'result') - pubsub = response.addElement((NS_PUBSUB, 'pubsub')) - subscription = pubsub.addElement('subscription') - subscription['node'] = 'test' - subscription['jid'] = 'user@example.org' - subscription['subscription'] = 'subscribed' - self.stub.send(response) - return d - - - def test_subscribeReturnsSubscription(self): - """ - A successful subscription should return a Subscription instance. - """ - def cb(subscription): - self.assertEqual(JID('user@example.org'), subscription.subscriber) - - d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', - JID('user@example.org')) - d.addCallback(cb) - - iq = self.stub.output[-1] - - response = toResponse(iq, 'result') - pubsub = response.addElement((NS_PUBSUB, 'pubsub')) - subscription = pubsub.addElement('subscription') - subscription['node'] = 'test' - subscription['jid'] = 'user@example.org' - subscription['subscription'] = 'subscribed' - self.stub.send(response) - return d - - - def test_subscribePending(self): - """ - Test sending subscription request that results in a pending - subscription. - """ - d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', - JID('user@example.org')) - - iq = self.stub.output[-1] - response = toResponse(iq, 'result') - command = response.addElement((NS_PUBSUB, 'pubsub')) - subscription = command.addElement('subscription') - subscription['node'] = 'test' - subscription['jid'] = 'user@example.org' - subscription['subscription'] = 'pending' - self.stub.send(response) - self.assertFailure(d, pubsub.SubscriptionPending) - return d - - - def test_subscribeUnconfigured(self): - """ - Test sending subscription request that results in an unconfigured - subscription. - """ - d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', - JID('user@example.org')) - - iq = self.stub.output[-1] - response = toResponse(iq, 'result') - command = response.addElement((NS_PUBSUB, 'pubsub')) - subscription = command.addElement('subscription') - subscription['node'] = 'test' - subscription['jid'] = 'user@example.org' - subscription['subscription'] = 'unconfigured' - self.stub.send(response) - self.assertFailure(d, pubsub.SubscriptionUnconfigured) - return d - - - def test_subscribeWithOptions(self): - options = {'pubsub#deliver': False} - - d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', - JID('user@example.org'), - options=options) - iq = self.stub.output[-1] - - # Check options present - childNames = [] - for element in iq.pubsub.elements(): - if element.uri == NS_PUBSUB: - childNames.append(element.name) - - self.assertEqual(['subscribe', 'options'], childNames) - form = data_form.findForm(iq.pubsub.options, - NS_PUBSUB_SUBSCRIBE_OPTIONS) - self.assertEqual('submit', form.formType) - form.typeCheck({'pubsub#deliver': {'type': 'boolean'}}) - self.assertEqual(options, form.getValues()) - - # Send response - response = toResponse(iq, 'result') - pubsub = response.addElement((NS_PUBSUB, 'pubsub')) - subscription = pubsub.addElement('subscription') - subscription['node'] = 'test' - subscription['jid'] = 'user@example.org' - subscription['subscription'] = 'subscribed' - self.stub.send(response) - - return d - - - def test_subscribeWithSender(self): - """ - Test sending subscription request from a specific JID. - """ - d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', - JID('user@example.org'), - sender=JID('user@example.org')) - - iq = self.stub.output[-1] - self.assertEquals('user@example.org', iq['from']) - - response = toResponse(iq, 'result') - pubsub = response.addElement((NS_PUBSUB, 'pubsub')) - subscription = pubsub.addElement('subscription') - subscription['node'] = 'test' - subscription['jid'] = 'user@example.org' - subscription['subscription'] = 'subscribed' - self.stub.send(response) - return d - - - def test_subscribeReturningSubscriptionIdentifier(self): - """ - Test sending subscription request with subscription identifier. - """ - def cb(subscription): - self.assertEqual('1234', subscription.subscriptionIdentifier) - - d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', - JID('user@example.org')) - d.addCallback(cb) - - iq = self.stub.output[-1] - - response = toResponse(iq, 'result') - pubsub = response.addElement((NS_PUBSUB, 'pubsub')) - subscription = pubsub.addElement('subscription') - subscription['node'] = 'test' - subscription['jid'] = 'user@example.org' - subscription['subscription'] = 'subscribed' - subscription['subid'] = '1234' - self.stub.send(response) - return d - - - def test_unsubscribe(self): - """ - Test sending unsubscription request. - """ - d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test', - JID('user@example.org')) - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('set', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'unsubscribe', NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - self.assertEquals('user@example.org', child['jid']) - - self.stub.send(toResponse(iq, 'result')) - return d - - - def test_unsubscribeWithSender(self): - """ - Test sending unsubscription request from a specific JID. - """ - d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test', - JID('user@example.org'), - sender=JID('user@example.org')) - - iq = self.stub.output[-1] - self.assertEquals('user@example.org', iq['from']) - self.stub.send(toResponse(iq, 'result')) - return d - - - def test_unsubscribeWithSubscriptionIdentifier(self): - """ - Test sending unsubscription request with subscription identifier. - """ - d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test', - JID('user@example.org'), - subscriptionIdentifier='1234') - - iq = self.stub.output[-1] - child = iq.pubsub.unsubscribe - self.assertEquals('1234', child['subid']) - - self.stub.send(toResponse(iq, 'result')) - return d - - - def test_items(self): - """ - Test sending items request. - """ - def cb(items): - self.assertEquals([], items) - - d = self.protocol.items(JID('pubsub.example.org'), 'test') - d.addCallback(cb) - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('get', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'items', NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - - response = toResponse(iq, 'result') - items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') - items['node'] = 'test' - - self.stub.send(response) - - return d - - - def test_itemsMaxItems(self): - """ - Test sending items request, with limit on the number of items. - """ - def cb(items): - self.assertEquals(2, len(items)) - self.assertEquals([item1, item2], items) - - d = self.protocol.items(JID('pubsub.example.org'), 'test', maxItems=2) - d.addCallback(cb) - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('get', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'items', NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - self.assertEquals('2', child['max_items']) - - response = toResponse(iq, 'result') - items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') - items['node'] = 'test' - item1 = items.addElement('item') - item1['id'] = 'item1' - item2 = items.addElement('item') - item2['id'] = 'item2' - - self.stub.send(response) - - return d - - - def test_itemsWithItemIdentifiers(self): - """ - Test sending items request with item identifiers. - """ - def cb(items): - self.assertEquals(2, len(items)) - self.assertEquals([item1, item2], items) - - d = self.protocol.items(JID('pubsub.example.org'), 'test', - itemIdentifiers=['item1', 'item2']) - d.addCallback(cb) - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('get', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'items', NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - itemIdentifiers = [item.getAttribute('id') for item in - domish.generateElementsQNamed(child.children, 'item', - NS_PUBSUB)] - self.assertEquals(['item1', 'item2'], itemIdentifiers) - - response = toResponse(iq, 'result') - items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') - items['node'] = 'test' - item1 = items.addElement('item') - item1['id'] = 'item1' - item2 = items.addElement('item') - item2['id'] = 'item2' - - self.stub.send(response) - - return d - - - def test_itemsWithSubscriptionIdentifier(self): - """ - Test sending items request with a subscription identifier. - """ - - d = self.protocol.items(JID('pubsub.example.org'), 'test', - subscriptionIdentifier='1234') - - iq = self.stub.output[-1] - child = iq.pubsub.items - self.assertEquals('1234', child['subid']) - - response = toResponse(iq, 'result') - items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') - items['node'] = 'test' - - self.stub.send(response) - return d - - - def test_itemsWithSender(self): - """ - Test sending items request from a specific JID. - """ - - d = self.protocol.items(JID('pubsub.example.org'), 'test', - sender=JID('user@example.org')) - - iq = self.stub.output[-1] - self.assertEquals('user@example.org', iq['from']) - - response = toResponse(iq, 'result') - items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') - items['node'] = 'test' - - self.stub.send(response) - return d - - - def test_retractItems(self): - """ - Test sending items retraction. - """ - d = self.protocol.retractItems(JID('pubsub.example.org'), 'test', - itemIdentifiers=['item1', 'item2']) - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('set', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'retract', NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - itemIdentifiers = [item.getAttribute('id') for item in - domish.generateElementsQNamed(child.children, 'item', - NS_PUBSUB)] - self.assertEquals(['item1', 'item2'], itemIdentifiers) - - self.stub.send(toResponse(iq, 'result')) - return d - - - def test_retractItemsWithSender(self): - """ - Test retracting items request from a specific JID. - """ - d = self.protocol.retractItems(JID('pubsub.example.org'), 'test', - itemIdentifiers=['item1', 'item2'], - sender=JID('user@example.org')) - - iq = self.stub.output[-1] - self.assertEquals('user@example.org', iq['from']) - - self.stub.send(toResponse(iq, 'result')) - return d - - - def test_getOptions(self): - def cb(form): - self.assertEqual('form', form.formType) - self.assertEqual(NS_PUBSUB_SUBSCRIBE_OPTIONS, form.formNamespace) - field = form.fields['pubsub#deliver'] - self.assertEqual('boolean', field.fieldType) - self.assertIdentical(True, field.value) - self.assertEqual('Enable delivery?', field.label) - - d = self.protocol.getOptions(JID('pubsub.example.org'), 'test', - JID('user@example.org'), - sender=JID('user@example.org')) - d.addCallback(cb) - - iq = self.stub.output[-1] - self.assertEqual('pubsub.example.org', iq.getAttribute('to')) - self.assertEqual('get', iq.getAttribute('type')) - self.assertEqual('pubsub', iq.pubsub.name) - self.assertEqual(NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'options', NS_PUBSUB)) - self.assertEqual(1, len(children)) - child = children[0] - self.assertEqual('test', child['node']) - - self.assertEqual(0, len(child.children)) - - # Send response - form = data_form.Form('form', formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) - form.addField(data_form.Field('boolean', var='pubsub#deliver', - label='Enable delivery?', - value=True)) - response = toResponse(iq, 'result') - response.addElement((NS_PUBSUB, 'pubsub')) - response.pubsub.addElement('options') - response.pubsub.options.addChild(form.toElement()) - self.stub.send(response) - - return d - - - def test_getOptionsWithSubscriptionIdentifier(self): - """ - Getting options with a subid should have the subid in the request. - """ - - d = self.protocol.getOptions(JID('pubsub.example.org'), 'test', - JID('user@example.org'), - sender=JID('user@example.org'), - subscriptionIdentifier='1234') - - iq = self.stub.output[-1] - child = iq.pubsub.options - self.assertEqual('1234', child['subid']) - - # Send response - form = data_form.Form('form', formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) - form.addField(data_form.Field('boolean', var='pubsub#deliver', - label='Enable delivery?', - value=True)) - response = toResponse(iq, 'result') - response.addElement((NS_PUBSUB, 'pubsub')) - response.pubsub.addElement('options') - response.pubsub.options.addChild(form.toElement()) - self.stub.send(response) - - return d - - - def test_setOptions(self): - """ - setOptions should send out a options-set request. - """ - options = {'pubsub#deliver': False} - - d = self.protocol.setOptions(JID('pubsub.example.org'), 'test', - JID('user@example.org'), - options, - sender=JID('user@example.org')) - - iq = self.stub.output[-1] - self.assertEqual('pubsub.example.org', iq.getAttribute('to')) - self.assertEqual('set', iq.getAttribute('type')) - self.assertEqual('pubsub', iq.pubsub.name) - self.assertEqual(NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'options', NS_PUBSUB)) - self.assertEqual(1, len(children)) - child = children[0] - self.assertEqual('test', child['node']) - - form = data_form.findForm(child, NS_PUBSUB_SUBSCRIBE_OPTIONS) - self.assertEqual('submit', form.formType) - form.typeCheck({'pubsub#deliver': {'type': 'boolean'}}) - self.assertEqual(options, form.getValues()) - - response = toResponse(iq, 'result') - self.stub.send(response) - - return d - - - def test_setOptionsWithSubscriptionIdentifier(self): - """ - setOptions should send out a options-set request with subid. - """ - options = {'pubsub#deliver': False} - - d = self.protocol.setOptions(JID('pubsub.example.org'), 'test', - JID('user@example.org'), - options, - subscriptionIdentifier='1234', - sender=JID('user@example.org')) - - iq = self.stub.output[-1] - child = iq.pubsub.options - self.assertEqual('1234', child['subid']) - - form = data_form.findForm(child, NS_PUBSUB_SUBSCRIBE_OPTIONS) - self.assertEqual('submit', form.formType) - form.typeCheck({'pubsub#deliver': {'type': 'boolean'}}) - self.assertEqual(options, form.getValues()) - - response = toResponse(iq, 'result') - self.stub.send(response) - - return d - - -class PubSubRequestTest(unittest.TestCase): - - def test_fromElementUnknown(self): - """ - An unknown verb raises NotImplementedError. - """ - - xml = """ - - - - - - """ - - self.assertRaises(NotImplementedError, - pubsub.PubSubRequest.fromElement, parseXml(xml)) - - - def test_fromElementKnownBadCombination(self): - """ - Multiple verbs in an unknown configuration raises NotImplementedError. - """ - - xml = """ - - - - - - - """ - - self.assertRaises(NotImplementedError, - pubsub.PubSubRequest.fromElement, parseXml(xml)) - - def test_fromElementPublish(self): - """ - Test parsing a publish request. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('publish', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - self.assertEqual('test', request.nodeIdentifier) - self.assertEqual([], request.items) - - - def test_fromElementPublishItems(self): - """ - Test parsing a publish request with items. - """ - - xml = """ - - - - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual(2, len(request.items)) - self.assertEqual(u'item1', request.items[0]["id"]) - self.assertEqual(u'item2', request.items[1]["id"]) - - - def test_fromElementPublishItemsOptions(self): - """ - Test parsing a publish request with items and options. - - Note that publishing options are not supported, but passing them - shouldn't affect processing of the publish request itself. - """ - - xml = """ - - - - - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual(2, len(request.items)) - self.assertEqual(u'item1', request.items[0]["id"]) - self.assertEqual(u'item2', request.items[1]["id"]) - - def test_fromElementPublishNoNode(self): - """ - A publish request to the root node should raise an exception. - """ - xml = """ - - - - - - """ - - err = self.assertRaises(error.StanzaError, - pubsub.PubSubRequest.fromElement, - parseXml(xml)) - self.assertEqual('bad-request', err.condition) - self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri) - self.assertEqual('nodeid-required', err.appCondition.name) - - - def test_fromElementSubscribe(self): - """ - Test parsing a subscription request. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('subscribe', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - self.assertEqual('test', request.nodeIdentifier) - self.assertEqual(JID('user@example.org/Home'), request.subscriber) - - - def test_fromElementSubscribeEmptyNode(self): - """ - Test parsing a subscription request to the root node. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('', request.nodeIdentifier) - - - def test_fromElementSubscribeNoJID(self): - """ - Subscribe requests without a JID should raise a bad-request exception. - """ - xml = """ - - - - - - """ - err = self.assertRaises(error.StanzaError, - pubsub.PubSubRequest.fromElement, - parseXml(xml)) - self.assertEqual('bad-request', err.condition) - self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri) - self.assertEqual('jid-required', err.appCondition.name) - - - def test_fromElementSubscribeWithOptions(self): - """ - Test parsing a subscription request. - """ - - xml = """ - - - - - - - http://jabber.org/protocol/pubsub#subscribe_options - - - 1 - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('subscribe', request.verb) - request.options.typeCheck({'pubsub#deliver': {'type': 'boolean'}}) - self.assertEqual({'pubsub#deliver': True}, request.options.getValues()) - - - def test_fromElementSubscribeWithOptionsBadFormType(self): - """ - The options form should have the right type. - """ - - xml = """ - - - - - - - http://jabber.org/protocol/pubsub#subscribe_options - - - 1 - - - - - - """ - - err = self.assertRaises(error.StanzaError, - pubsub.PubSubRequest.fromElement, - parseXml(xml)) - self.assertEqual('bad-request', err.condition) - self.assertEqual("Unexpected form type 'result'", err.text) - self.assertEqual(None, err.appCondition) - - - def test_fromElementSubscribeWithOptionsEmpty(self): - """ - When no (suitable) form is found, the options are empty. - """ - - xml = """ - - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('subscribe', request.verb) - self.assertEqual({}, request.options.getValues()) - - - def test_fromElementUnsubscribe(self): - """ - Test parsing an unsubscription request. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('unsubscribe', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - self.assertEqual('test', request.nodeIdentifier) - self.assertEqual(JID('user@example.org/Home'), request.subscriber) - - - def test_fromElementUnsubscribeWithSubscriptionIdentifier(self): - """ - Test parsing an unsubscription request with subscription identifier. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('1234', request.subscriptionIdentifier) - - - def test_fromElementUnsubscribeNoJID(self): - """ - Unsubscribe requests without a JID should raise a bad-request exception. - """ - xml = """ - - - - - - """ - err = self.assertRaises(error.StanzaError, - pubsub.PubSubRequest.fromElement, - parseXml(xml)) - self.assertEqual('bad-request', err.condition) - self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri) - self.assertEqual('jid-required', err.appCondition.name) - - - def test_fromElementOptionsGet(self): - """ - Test parsing a request for getting subscription options. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('optionsGet', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - self.assertEqual('test', request.nodeIdentifier) - self.assertEqual(JID('user@example.org/Home'), request.subscriber) - - - def test_fromElementOptionsGetWithSubscriptionIdentifier(self): - """ - Test parsing a request for getting subscription options with subid. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('1234', request.subscriptionIdentifier) - - - def test_fromElementOptionsSet(self): - """ - Test parsing a request for setting subscription options. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#subscribe_options - - 1 - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('optionsSet', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - self.assertEqual('test', request.nodeIdentifier) - self.assertEqual(JID('user@example.org/Home'), request.subscriber) - self.assertEqual({'pubsub#deliver': '1'}, request.options.getValues()) - - - def test_fromElementOptionsSetWithSubscriptionIdentifier(self): - """ - Test parsing a request for setting subscription options with subid. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#subscribe_options - - 1 - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('1234', request.subscriptionIdentifier) - - - def test_fromElementOptionsSetCancel(self): - """ - Test parsing a request for cancelling setting subscription options. - """ - - xml = """ - - - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('cancel', request.options.formType) - - - def test_fromElementOptionsSetBadFormType(self): - """ - On a options set request unknown fields should be ignored. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#subscribe_options - - 1 - - - - - """ - - err = self.assertRaises(error.StanzaError, - pubsub.PubSubRequest.fromElement, - parseXml(xml)) - self.assertEqual('bad-request', err.condition) - self.assertEqual("Unexpected form type 'result'", err.text) - self.assertEqual(None, err.appCondition) - - - def test_fromElementOptionsSetNoForm(self): - """ - On a options set request a form is required. - """ - - xml = """ - - - - - - """ - err = self.assertRaises(error.StanzaError, - pubsub.PubSubRequest.fromElement, - parseXml(xml)) - self.assertEqual('bad-request', err.condition) - self.assertEqual(None, err.appCondition) - - - def test_fromElementSubscriptions(self): - """ - Test parsing a request for all subscriptions. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('subscriptions', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - - - def test_fromElementAffiliations(self): - """ - Test parsing a request for all affiliations. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('affiliations', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - - - def test_fromElementCreate(self): - """ - Test parsing a request to create a node. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('create', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - self.assertEqual('mynode', request.nodeIdentifier) - self.assertIdentical(None, request.options) - - - def test_fromElementCreateInstant(self): - """ - Test parsing a request to create an instant node. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertIdentical(None, request.nodeIdentifier) - - - def test_fromElementCreateConfigureEmpty(self): - """ - Test parsing a request to create a node with an empty configuration. - """ - - xml = """ - - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual({}, request.options.getValues()) - self.assertEqual(u'mynode', request.nodeIdentifier) - - - def test_fromElementCreateConfigureEmptyWrongOrder(self): - """ - Test parsing a request to create a node and configure, wrong order. - - The C{configure} element should come after the C{create} request, - but we should accept both orders. - """ - - xml = """ - - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual({}, request.options.getValues()) - self.assertEqual(u'mynode', request.nodeIdentifier) - - - def test_fromElementCreateConfigure(self): - """ - Test parsing a request to create a node. - """ - - xml = """ - - - - - - - http://jabber.org/protocol/pubsub#node_config - - open - 0 - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - values = request.options - self.assertIn('pubsub#access_model', values) - self.assertEqual(u'open', values['pubsub#access_model']) - self.assertIn('pubsub#persist_items', values) - self.assertEqual(u'0', values['pubsub#persist_items']) - - - def test_fromElementCreateConfigureBadFormType(self): - """ - The form of a node creation request should have the right type. - """ - - xml = """ - - - - - - - http://jabber.org/protocol/pubsub#node_config - - open - 0 - - - - - """ - - err = self.assertRaises(error.StanzaError, - pubsub.PubSubRequest.fromElement, - parseXml(xml)) - self.assertEqual('bad-request', err.condition) - self.assertEqual("Unexpected form type 'result'", err.text) - self.assertEqual(None, err.appCondition) - - - def test_fromElementDefault(self): - """ - Parsing default node configuration request sets required attributes. - - Besides C{verb}, C{sender} and C{recipient}, we expect C{nodeType} - to be set. If not passed it receives the default C{u'leaf'}. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEquals(u'default', request.verb) - self.assertEquals(JID('user@example.org'), request.sender) - self.assertEquals(JID('pubsub.example.org'), request.recipient) - self.assertEquals(u'leaf', request.nodeType) - - - def test_fromElementDefaultCollection(self): - """ - Parsing default request for collection sets nodeType to collection. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#node_config - - - collection - - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEquals('collection', request.nodeType) - - - def test_fromElementConfigureGet(self): - """ - Test parsing a node configuration get request. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('configureGet', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - self.assertEqual('test', request.nodeIdentifier) - - - def test_fromElementConfigureSet(self): - """ - On a node configuration set request the Data Form is parsed. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#node_config - - 0 - 1 - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('configureSet', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - self.assertEqual('test', request.nodeIdentifier) - self.assertEqual({'pubsub#deliver_payloads': '0', - 'pubsub#persist_items': '1'}, - request.options.getValues()) - - - def test_fromElementConfigureSetCancel(self): - """ - The node configuration is cancelled, so no options. - """ - - xml = """ - - - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('cancel', request.options.formType) - - - def test_fromElementConfigureSetBadFormType(self): - """ - The form of a node configuraton set request should have the right type. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#node_config - - 0 - 1 - - - - - """ - - err = self.assertRaises(error.StanzaError, - pubsub.PubSubRequest.fromElement, - parseXml(xml)) - self.assertEqual('bad-request', err.condition) - self.assertEqual("Unexpected form type 'result'", err.text) - self.assertEqual(None, err.appCondition) - - - def test_fromElementConfigureSetNoForm(self): - """ - On a node configuration set request a form is required. - """ - - xml = """ - - - - - - """ - err = self.assertRaises(error.StanzaError, - pubsub.PubSubRequest.fromElement, - parseXml(xml)) - self.assertEqual('bad-request', err.condition) - self.assertEqual(None, err.appCondition) - - - def test_fromElementItems(self): - """ - Test parsing an items request. - """ - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('items', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - self.assertEqual('test', request.nodeIdentifier) - self.assertIdentical(None, request.maxItems) - self.assertIdentical(None, request.subscriptionIdentifier) - self.assertEqual([], request.itemIdentifiers) - - - def test_fromElementItemsSubscriptionIdentifier(self): - """ - Test parsing an items request with subscription identifier. - """ - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('1234', request.subscriptionIdentifier) - - - def test_fromElementRetract(self): - """ - Test parsing a retract request. - """ - - xml = """ - - - - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('retract', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - self.assertEqual('test', request.nodeIdentifier) - self.assertEqual(['item1', 'item2'], request.itemIdentifiers) - - - def test_fromElementPurge(self): - """ - Test parsing a purge request. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('purge', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - self.assertEqual('test', request.nodeIdentifier) - - - def test_fromElementDelete(self): - """ - Test parsing a delete request. - """ - - xml = """ - - - - - - """ - - request = pubsub.PubSubRequest.fromElement(parseXml(xml)) - self.assertEqual('delete', request.verb) - self.assertEqual(JID('user@example.org'), request.sender) - self.assertEqual(JID('pubsub.example.org'), request.recipient) - self.assertEqual('test', request.nodeIdentifier) - - - -class PubSubServiceTest(unittest.TestCase, TestableRequestHandlerMixin): - """ - Tests for L{pubsub.PubSubService}. - """ - - def setUp(self): - self.stub = XmlStreamStub() - self.resource = pubsub.PubSubResource() - self.service = pubsub.PubSubService(self.resource) - self.service.send = self.stub.xmlstream.send - - def test_interface(self): - """ - Do instances of L{pubsub.PubSubService} provide L{iwokkel.IPubSubService}? - """ - verify.verifyObject(iwokkel.IPubSubService, self.service) - - - def test_interfaceIDisco(self): - """ - Do instances of L{pubsub.PubSubService} provide L{iwokkel.IDisco}? - """ - verify.verifyObject(iwokkel.IDisco, self.service) - - - def test_connectionMade(self): - """ - Verify setup of observers in L{pubsub.connectionMade}. - """ - requests = [] - - def handleRequest(iq): - requests.append(iq) - - self.service.xmlstream = self.stub.xmlstream - self.service.handleRequest = handleRequest - self.service.connectionMade() - - for namespace in (NS_PUBSUB, NS_PUBSUB_OWNER): - for stanzaType in ('get', 'set'): - iq = domish.Element((None, 'iq')) - iq['type'] = stanzaType - iq.addElement((namespace, 'pubsub')) - self.stub.xmlstream.dispatch(iq) - - self.assertEqual(4, len(requests)) - - - def test_getDiscoInfo(self): - """ - Test getDiscoInfo calls getNodeInfo and returns some minimal info. - """ - def cb(info): - discoInfo = disco.DiscoInfo() - for item in info: - discoInfo.append(item) - self.assertIn(('pubsub', 'service'), discoInfo.identities) - self.assertIn(disco.NS_DISCO_ITEMS, discoInfo.features) - - d = self.service.getDiscoInfo(JID('user@example.org/home'), - JID('pubsub.example.org'), '') - d.addCallback(cb) - return d - - - def test_getDiscoInfoNodeType(self): - """ - Test getDiscoInfo with node type. - """ - def cb(info): - discoInfo = disco.DiscoInfo() - for item in info: - discoInfo.append(item) - self.assertIn(('pubsub', 'collection'), discoInfo.identities) - - def getInfo(requestor, target, nodeIdentifier): - return defer.succeed({'type': 'collection', - 'meta-data': {}}) - - self.resource.getInfo = getInfo - d = self.service.getDiscoInfo(JID('user@example.org/home'), - JID('pubsub.example.org'), '') - d.addCallback(cb) - return d - - - def test_getDiscoInfoMetaData(self): - """ - Test getDiscoInfo with returned meta data. - """ - def cb(info): - discoInfo = disco.DiscoInfo() - for item in info: - discoInfo.append(item) - - self.assertIn(('pubsub', 'leaf'), discoInfo.identities) - self.assertIn(NS_PUBSUB_META_DATA, discoInfo.extensions) - form = discoInfo.extensions[NS_PUBSUB_META_DATA] - self.assertIn('pubsub#node_type', form.fields) - - def getInfo(requestor, target, nodeIdentifier): - metaData = [{'var': 'pubsub#persist_items', - 'label': 'Persist items to storage', - 'value': True}] - return defer.succeed({'type': 'leaf', 'meta-data': metaData}) - - self.resource.getInfo = getInfo - d = self.service.getDiscoInfo(JID('user@example.org/home'), - JID('pubsub.example.org'), '') - d.addCallback(cb) - return d - - - def test_getDiscoInfoResourceFeatures(self): - """ - Test getDiscoInfo with the resource features. - """ - def cb(info): - discoInfo = disco.DiscoInfo() - for item in info: - discoInfo.append(item) - self.assertIn('http://jabber.org/protocol/pubsub#publish', - discoInfo.features) - - self.resource.features = ['publish'] - d = self.service.getDiscoInfo(JID('user@example.org/home'), - JID('pubsub.example.org'), '') - d.addCallback(cb) - return d - - - def test_getDiscoInfoBadResponse(self): - """ - If getInfo returns invalid response, it should be logged, then ignored. - """ - def cb(info): - self.assertEquals([], info) - self.assertEqual(1, len(self.flushLoggedErrors(TypeError))) - - def getInfo(requestor, target, nodeIdentifier): - return defer.succeed('bad response') - - self.resource.getInfo = getInfo - d = self.service.getDiscoInfo(JID('user@example.org/home'), - JID('pubsub.example.org'), 'test') - d.addCallback(cb) - return d - - - def test_getDiscoInfoException(self): - """ - If getInfo returns invalid response, it should be logged, then ignored. - """ - def cb(info): - self.assertEquals([], info) - self.assertEqual(1, len(self.flushLoggedErrors(NotImplementedError))) - - def getInfo(requestor, target, nodeIdentifier): - return defer.fail(NotImplementedError()) - - self.resource.getInfo = getInfo - d = self.service.getDiscoInfo(JID('user@example.org/home'), - JID('pubsub.example.org'), 'test') - d.addCallback(cb) - return d - - - def test_getDiscoItemsRoot(self): - """ - Test getDiscoItems on the root node. - """ - def getNodes(requestor, service, nodeIdentifier): - return defer.succeed(['node1', 'node2']) - - def cb(items): - self.assertEqual(2, len(items)) - item1, item2 = items - - self.assertEqual(JID('pubsub.example.org'), item1.entity) - self.assertEqual('node1', item1.nodeIdentifier) - - self.assertEqual(JID('pubsub.example.org'), item2.entity) - self.assertEqual('node2', item2.nodeIdentifier) - - self.resource.getNodes = getNodes - d = self.service.getDiscoItems(JID('user@example.org/home'), - JID('pubsub.example.org'), - '') - d.addCallback(cb) - return d - - - def test_getDiscoItemsRootHideNodes(self): - """ - Test getDiscoItems on the root node. - """ - def getNodes(requestor, service, nodeIdentifier): - raise Exception("Unexpected call to getNodes") - - def cb(items): - self.assertEqual([], items) - - self.service.hideNodes = True - self.resource.getNodes = getNodes - d = self.service.getDiscoItems(JID('user@example.org/home'), - JID('pubsub.example.org'), - '') - d.addCallback(cb) - return d - - - def test_getDiscoItemsNonRoot(self): - """ - Test getDiscoItems on a non-root node. - """ - def getNodes(requestor, service, nodeIdentifier): - return defer.succeed(['node1', 'node2']) - - def cb(items): - self.assertEqual(2, len(items)) - - self.resource.getNodes = getNodes - d = self.service.getDiscoItems(JID('user@example.org/home'), - JID('pubsub.example.org'), - 'test') - d.addCallback(cb) - return d - - - def test_on_publish(self): - """ - A publish request should result in L{PubSubService.publish} being - called. - """ - - xml = """ - - - - - - """ - - def publish(request): - return defer.succeed(None) - - self.resource.publish = publish - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - return self.handleRequest(xml) - - - def test_on_subscribe(self): - """ - A successful subscription should return the current subscription. - """ - - xml = """ - - - - - - """ - - def subscribe(request): - return defer.succeed(pubsub.Subscription(request.nodeIdentifier, - request.subscriber, - 'subscribed')) - - def cb(element): - self.assertEqual('pubsub', element.name) - self.assertEqual(NS_PUBSUB, element.uri) - subscription = element.subscription - self.assertEqual(NS_PUBSUB, subscription.uri) - self.assertEqual('test', subscription['node']) - self.assertEqual('user@example.org/Home', subscription['jid']) - self.assertEqual('subscribed', subscription['subscription']) - - self.resource.subscribe = subscribe - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_subscribeEmptyNode(self): - """ - A successful subscription on root node should return no node attribute. - """ - - xml = """ - - - - - - """ - - def subscribe(request): - return defer.succeed(pubsub.Subscription(request.nodeIdentifier, - request.subscriber, - 'subscribed')) - - def cb(element): - self.assertFalse(element.subscription.hasAttribute('node')) - - self.resource.subscribe = subscribe - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_subscribeSubscriptionIdentifier(self): - """ - If a subscription returns a subid, this should be available. - """ - - xml = """ - - - - - - """ - - def subscribe(request): - subscription = pubsub.Subscription(request.nodeIdentifier, - request.subscriber, - 'subscribed', - subscriptionIdentifier='1234') - return defer.succeed(subscription) - - def cb(element): - self.assertEqual('1234', element.subscription.getAttribute('subid')) - - self.resource.subscribe = subscribe - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_unsubscribe(self): - """ - A successful unsubscription should return an empty response. - """ - - xml = """ - - - - - - """ - - def unsubscribe(request): - return defer.succeed(None) - - def cb(element): - self.assertIdentical(None, element) - - self.resource.unsubscribe = unsubscribe - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_unsubscribeSubscriptionIdentifier(self): - """ - A successful unsubscription with subid should return an empty response. - """ - - xml = """ - - - - - - """ - - def unsubscribe(request): - self.assertEqual('1234', request.subscriptionIdentifier) - return defer.succeed(None) - - def cb(element): - self.assertIdentical(None, element) - - self.resource.unsubscribe = unsubscribe - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_optionsGet(self): - """ - Getting subscription options is not supported. - """ - - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_on_optionsSet(self): - """ - Setting subscription options is not supported. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#subscribe_options - - 1 - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_on_subscriptions(self): - """ - A subscriptions request should result in - L{PubSubService.subscriptions} being called and the result prepared - for the response. - """ - - xml = """ - - - - - - """ - - def subscriptions(request): - subscription = pubsub.Subscription('test', JID('user@example.org'), - 'subscribed') - return defer.succeed([subscription]) - - def cb(element): - self.assertEqual('pubsub', element.name) - self.assertEqual(NS_PUBSUB, element.uri) - self.assertEqual(NS_PUBSUB, element.subscriptions.uri) - children = list(element.subscriptions.elements()) - self.assertEqual(1, len(children)) - subscription = children[0] - self.assertEqual('subscription', subscription.name) - self.assertEqual(NS_PUBSUB, subscription.uri, NS_PUBSUB) - self.assertEqual('user@example.org', subscription['jid']) - self.assertEqual('test', subscription['node']) - self.assertEqual('subscribed', subscription['subscription']) - - self.resource.subscriptions = subscriptions - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_subscriptionsWithSubscriptionIdentifier(self): - """ - A subscriptions request response should include subids, if set. - """ - - xml = """ - - - - - - """ - - def subscriptions(request): - subscription = pubsub.Subscription('test', JID('user@example.org'), - 'subscribed', - subscriptionIdentifier='1234') - return defer.succeed([subscription]) - - def cb(element): - subscription = element.subscriptions.subscription - self.assertEqual('1234', subscription['subid']) - - self.resource.subscriptions = subscriptions - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_affiliations(self): - """ - A subscriptions request should result in - L{PubSubService.affiliations} being called and the result prepared - for the response. - """ - - xml = """ - - - - - - """ - - def affiliations(request): - affiliation = ('test', 'owner') - return defer.succeed([affiliation]) - - def cb(element): - self.assertEqual('pubsub', element.name) - self.assertEqual(NS_PUBSUB, element.uri) - self.assertEqual(NS_PUBSUB, element.affiliations.uri) - children = list(element.affiliations.elements()) - self.assertEqual(1, len(children)) - affiliation = children[0] - self.assertEqual('affiliation', affiliation.name) - self.assertEqual(NS_PUBSUB, affiliation.uri) - self.assertEqual('test', affiliation['node']) - self.assertEqual('owner', affiliation['affiliation']) - - self.resource.affiliations = affiliations - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_create(self): - """ - Replies to create node requests don't return the created node. - """ - - xml = """ - - - - - - """ - - def create(request): - return defer.succeed(request.nodeIdentifier) - - def cb(element): - self.assertIdentical(None, element) - - self.resource.create = create - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_createChanged(self): - """ - Replies to create node requests return the created node if changed. - """ - - xml = """ - - - - - - """ - - def create(request): - return defer.succeed(u'myrenamednode') - - def cb(element): - self.assertEqual('pubsub', element.name) - self.assertEqual(NS_PUBSUB, element.uri) - self.assertEqual(NS_PUBSUB, element.create.uri) - self.assertEqual(u'myrenamednode', - element.create.getAttribute('node')) - - self.resource.create = create - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_createInstant(self): - """ - Replies to create instant node requests return the created node. - """ - - xml = """ - - - - - - """ - - def create(request): - return defer.succeed(u'random') - - def cb(element): - self.assertEqual('pubsub', element.name) - self.assertEqual(NS_PUBSUB, element.uri) - self.assertEqual(NS_PUBSUB, element.create.uri) - self.assertEqual(u'random', element.create.getAttribute('node')) - - self.resource.create = create - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_createWithConfig(self): - """ - On a node create with configuration request the Data Form is parsed and - L{PubSubResource.create} is called with the passed options. - """ - - xml = """ - - - - - - - http://jabber.org/protocol/pubsub#node_config - - 0 - 1 - - - - - """ - - def getConfigurationOptions(): - return { - "pubsub#persist_items": - {"type": "boolean", - "label": "Persist items to storage"}, - "pubsub#deliver_payloads": - {"type": "boolean", - "label": "Deliver payloads with event notifications"} - } - - def create(request): - self.assertEqual({'pubsub#deliver_payloads': False, - 'pubsub#persist_items': True}, - request.options.getValues()) - return defer.succeed(None) - - self.resource.getConfigurationOptions = getConfigurationOptions - self.resource.create = create - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - return self.handleRequest(xml) - - - def test_on_default(self): - """ - A default request returns default options filtered by available fields. - """ - - xml = """ - - - - - - """ - fieldDefs = { - "pubsub#persist_items": - {"type": "boolean", - "label": "Persist items to storage"}, - "pubsub#deliver_payloads": - {"type": "boolean", - "label": "Deliver payloads with event notifications"} - } - - def getConfigurationOptions(): - return fieldDefs - - def default(request): - return defer.succeed({'pubsub#persist_items': 'false', - 'x-myfield': '1'}) - - def cb(element): - self.assertEquals('pubsub', element.name) - self.assertEquals(NS_PUBSUB_OWNER, element.uri) - self.assertEquals(NS_PUBSUB_OWNER, element.default.uri) - form = data_form.Form.fromElement(element.default.x) - self.assertEquals(NS_PUBSUB_NODE_CONFIG, form.formNamespace) - form.typeCheck(fieldDefs) - self.assertIn('pubsub#persist_items', form.fields) - self.assertFalse(form.fields['pubsub#persist_items'].value) - self.assertNotIn('x-myfield', form.fields) - - self.resource.getConfigurationOptions = getConfigurationOptions - self.resource.default = default - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_defaultUnknownNodeType(self): - """ - Unknown node types yield non-acceptable. - - Both C{getConfigurationOptions} and C{default} must not be called. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#node_config - - - unknown - - - - - - - """ - - def getConfigurationOptions(): - self.fail("Unexpected call to getConfigurationOptions") - - def default(request): - self.fail("Unexpected call to default") - - def cb(result): - self.assertEquals('not-acceptable', result.condition) - - self.resource.getConfigurationOptions = getConfigurationOptions - self.resource.default = default - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_on_configureGet(self): - """ - On a node configuration get - requestL{PubSubResource.configureGet} is called and results in a - data form with the configuration. - """ - - xml = """ - - - - - - """ - - def getConfigurationOptions(): - return { - "pubsub#persist_items": - {"type": "boolean", - "label": "Persist items to storage"}, - "pubsub#deliver_payloads": - {"type": "boolean", - "label": "Deliver payloads with event notifications"}, - "pubsub#owner": - {"type": "jid-single", - "label": "Owner of the node"} - } - - def configureGet(request): - return defer.succeed({'pubsub#deliver_payloads': '0', - 'pubsub#persist_items': '1', - 'pubsub#owner': JID('user@example.org'), - 'x-myfield': 'a'}) - - def cb(element): - self.assertEqual('pubsub', element.name) - self.assertEqual(NS_PUBSUB_OWNER, element.uri) - self.assertEqual(NS_PUBSUB_OWNER, element.configure.uri) - form = data_form.Form.fromElement(element.configure.x) - self.assertEqual(NS_PUBSUB_NODE_CONFIG, form.formNamespace) - fields = form.fields - - self.assertIn('pubsub#deliver_payloads', fields) - field = fields['pubsub#deliver_payloads'] - self.assertEqual('boolean', field.fieldType) - field.typeCheck() - self.assertEqual(False, field.value) - - self.assertIn('pubsub#persist_items', fields) - field = fields['pubsub#persist_items'] - self.assertEqual('boolean', field.fieldType) - field.typeCheck() - self.assertEqual(True, field.value) - - self.assertIn('pubsub#owner', fields) - field = fields['pubsub#owner'] - self.assertEqual('jid-single', field.fieldType) - field.typeCheck() - self.assertEqual(JID('user@example.org'), field.value) - - self.assertNotIn('x-myfield', fields) - - self.resource.getConfigurationOptions = getConfigurationOptions - self.resource.configureGet = configureGet - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_configureSet(self): - """ - On a node configuration set request the Data Form is parsed and - L{PubSubResource.configureSet} is called with the passed options. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#node_config - - 0 - 1 - - - - - """ - - def getConfigurationOptions(): - return { - "pubsub#persist_items": - {"type": "boolean", - "label": "Persist items to storage"}, - "pubsub#deliver_payloads": - {"type": "boolean", - "label": "Deliver payloads with event notifications"} - } - - def configureSet(request): - self.assertEqual({'pubsub#deliver_payloads': False, - 'pubsub#persist_items': True}, - request.options.getValues()) - return defer.succeed(None) - - self.resource.getConfigurationOptions = getConfigurationOptions - self.resource.configureSet = configureSet - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - return self.handleRequest(xml) - - - def test_on_configureSetCancel(self): - """ - The node configuration is cancelled, - L{PubSubResource.configureSet} not called. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#node_config - - - - - - """ - - def configureSet(request): - self.fail("Unexpected call to setConfiguration") - - self.resource.configureSet = configureSet - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - return self.handleRequest(xml) - - - def test_on_configureSetIgnoreUnknown(self): - """ - On a node configuration set request unknown fields should be ignored. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#node_config - - 0 - 1 - - - - - """ - - def getConfigurationOptions(): - return { - "pubsub#persist_items": - {"type": "boolean", - "label": "Persist items to storage"}, - "pubsub#deliver_payloads": - {"type": "boolean", - "label": "Deliver payloads with event notifications"} - } - - def configureSet(request): - self.assertEquals(['pubsub#deliver_payloads'], - request.options.keys()) - - self.resource.getConfigurationOptions = getConfigurationOptions - self.resource.configureSet = configureSet - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - return self.handleRequest(xml) - - - def test_on_configureSetBadFormType(self): - """ - On a node configuration set request unknown fields should be ignored. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#node_config - - 0 - 1 - - - - - """ - - def cb(result): - self.assertEquals('bad-request', result.condition) - self.assertEqual("Unexpected form type 'result'", result.text) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_on_items(self): - """ - On a items request, return all items for the given node. - """ - xml = """ - - - - - - """ - - def items(request): - return defer.succeed([pubsub.Item('current')]) - - def cb(element): - self.assertEqual(NS_PUBSUB, element.uri) - self.assertEqual(NS_PUBSUB, element.items.uri) - self.assertEqual(1, len(element.items.children)) - item = element.items.children[-1] - self.assertTrue(domish.IElement.providedBy(item)) - self.assertEqual('item', item.name) - self.assertEqual(NS_PUBSUB, item.uri) - self.assertEqual('current', item['id']) - - self.resource.items = items - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_retract(self): - """ - A retract request should result in L{PubSubResource.retract} - being called. - """ - - xml = """ - - - - - - - - - """ - - def retract(request): - return defer.succeed(None) - - self.resource.retract = retract - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - return self.handleRequest(xml) - - - def test_on_purge(self): - """ - A purge request should result in L{PubSubResource.purge} being - called. - """ - - xml = """ - - - - - - """ - - def purge(request): - return defer.succeed(None) - - self.resource.purge = purge - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - return self.handleRequest(xml) - - - def test_on_delete(self): - """ - A delete request should result in L{PubSubResource.delete} being - called. - """ - - xml = """ - - - - - - """ - - def delete(request): - return defer.succeed(None) - - self.resource.delete = delete - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - return self.handleRequest(xml) - - - def test_notifyPublish(self): - """ - Publish notifications are sent to the subscribers. - """ - subscriber = JID('user@example.org') - subscriptions = [pubsub.Subscription('test', subscriber, 'subscribed')] - items = [pubsub.Item('current')] - notifications = [(subscriber, subscriptions, items)] - self.service.notifyPublish(JID('pubsub.example.org'), 'test', - notifications) - message = self.stub.output[-1] - - self.assertEquals('message', message.name) - self.assertIdentical(None, message.uri) - self.assertEquals('user@example.org', message['to']) - self.assertEquals('pubsub.example.org', message['from']) - self.assertTrue(message.event) - self.assertEquals(NS_PUBSUB_EVENT, message.event.uri) - self.assertTrue(message.event.items) - self.assertEquals(NS_PUBSUB_EVENT, message.event.items.uri) - self.assertTrue(message.event.items.hasAttribute('node')) - self.assertEquals('test', message.event.items['node']) - itemElements = list(domish.generateElementsQNamed( - message.event.items.children, 'item', NS_PUBSUB_EVENT)) - self.assertEquals(1, len(itemElements)) - self.assertEquals('current', itemElements[0].getAttribute('id')) - - - def test_notifyPublishCollection(self): - """ - Publish notifications are sent to the subscribers of collections. - - The node the item was published to is on the C{items} element, while - the subscribed-to node is in the C{'Collections'} SHIM header. - """ - subscriber = JID('user@example.org') - subscriptions = [pubsub.Subscription('', subscriber, 'subscribed')] - items = [pubsub.Item('current')] - notifications = [(subscriber, subscriptions, items)] - self.service.notifyPublish(JID('pubsub.example.org'), 'test', - notifications) - message = self.stub.output[-1] - - self.assertTrue(message.event.items.hasAttribute('node')) - self.assertEquals('test', message.event.items['node']) - headers = shim.extractHeaders(message) - self.assertIn('Collection', headers) - self.assertIn('', headers['Collection']) - - - def test_notifyDelete(self): - """ - Subscribers should be sent a delete notification. - """ - subscriptions = [JID('user@example.org')] - self.service.notifyDelete(JID('pubsub.example.org'), 'test', - subscriptions) - message = self.stub.output[-1] - - self.assertEquals('message', message.name) - self.assertIdentical(None, message.uri) - self.assertEquals('user@example.org', message['to']) - self.assertEquals('pubsub.example.org', message['from']) - self.assertTrue(message.event) - self.assertEqual(NS_PUBSUB_EVENT, message.event.uri) - self.assertTrue(message.event.delete) - self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.uri) - self.assertTrue(message.event.delete.hasAttribute('node')) - self.assertEqual('test', message.event.delete['node']) - - - def test_notifyDeleteRedirect(self): - """ - Subscribers should be sent a delete notification with redirect. - """ - redirectURI = 'xmpp:pubsub.example.org?;node=test2' - subscriptions = [JID('user@example.org')] - self.service.notifyDelete(JID('pubsub.example.org'), 'test', - subscriptions, redirectURI) - message = self.stub.output[-1] - - self.assertEquals('message', message.name) - self.assertIdentical(None, message.uri) - self.assertEquals('user@example.org', message['to']) - self.assertEquals('pubsub.example.org', message['from']) - self.assertTrue(message.event) - self.assertEqual(NS_PUBSUB_EVENT, message.event.uri) - self.assertTrue(message.event.delete) - self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.uri) - self.assertTrue(message.event.delete.hasAttribute('node')) - self.assertEqual('test', message.event.delete['node']) - self.assertTrue(message.event.delete.redirect) - self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.redirect.uri) - self.assertTrue(message.event.delete.redirect.hasAttribute('uri')) - self.assertEqual(redirectURI, message.event.delete.redirect['uri']) - - - def test_on_subscriptionsGet(self): - """ - Getting subscription options is not supported. - """ - - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('manage-subscriptions', - result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_on_subscriptionsSet(self): - """ - Setting subscription options is not supported. - """ - - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('manage-subscriptions', - result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_on_affiliationsGet(self): - """ - Getting node affiliations should have. - """ - - xml = """ - - - - - - """ - - def affiliationsGet(request): - self.assertEquals('test', request.nodeIdentifier) - return defer.succeed({JID('user@example.org'): 'owner'}) - - def cb(element): - self.assertEquals(u'pubsub', element.name) - self.assertEquals(NS_PUBSUB_OWNER, element.uri) - self.assertEquals(NS_PUBSUB_OWNER, element.affiliations.uri) - self.assertEquals(u'test', element.affiliations[u'node']) - children = list(element.affiliations.elements()) - self.assertEquals(1, len(children)) - affiliation = children[0] - self.assertEquals(u'affiliation', affiliation.name) - self.assertEquals(NS_PUBSUB_OWNER, affiliation.uri) - self.assertEquals(u'user@example.org', affiliation[u'jid']) - self.assertEquals(u'owner', affiliation[u'affiliation']) - - self.resource.affiliationsGet = affiliationsGet - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_affiliationsGetEmptyNode(self): - """ - Getting node affiliations without node should assume empty node. - """ - - xml = """ - - - - - - """ - - def affiliationsGet(request): - self.assertEqual('', request.nodeIdentifier) - return defer.succeed({}) - - def cb(element): - self.assertFalse(element.affiliations.hasAttribute(u'node')) - - self.resource.affiliationsGet = affiliationsGet - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - - def test_on_affiliationsSet(self): - """ - Setting node affiliations has the affiliations to be modified. - """ - - xml = """ - - - - - - - - """ - - def affiliationsSet(request): - self.assertEquals(u'test', request.nodeIdentifier) - otherJID = JID(u'other@example.org') - self.assertIn(otherJID, request.affiliations) - self.assertEquals(u'publisher', request.affiliations[otherJID]) - - self.resource.affiliationsSet = affiliationsSet - return self.handleRequest(xml) - - - def test_on_affiliationsSetBareJID(self): - """ - Affiliations are always on the bare JID. - """ - - xml = """ - - - - - - - - """ - - def affiliationsSet(request): - otherJID = JID(u'other@example.org') - self.assertIn(otherJID, request.affiliations) - - self.resource.affiliationsSet = affiliationsSet - return self.handleRequest(xml) - - - def test_on_affiliationsSetMultipleForSameEntity(self): - """ - Setting node affiliations can only have one item per entity. - """ - - xml = """ - - - - - - - - - """ - - def cb(result): - self.assertEquals('bad-request', result.condition) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_on_affiliationsSetMissingJID(self): - """ - Setting node affiliations must include a JID per affiliation. - """ - - xml = """ - - - - - - - - """ - - def cb(result): - self.assertEquals('bad-request', result.condition) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_on_affiliationsSetMissingAffiliation(self): - """ - Setting node affiliations must include an affiliation. - """ - - xml = """ - - - - - - - - """ - - def cb(result): - self.assertEquals('bad-request', result.condition) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - -class PubSubServiceWithoutResourceTest(unittest.TestCase, TestableRequestHandlerMixin): - - def setUp(self): - self.stub = XmlStreamStub() - self.service = pubsub.PubSubService() - self.service.send = self.stub.xmlstream.send - - - def test_getDiscoInfo(self): - """ - Test getDiscoInfo calls getNodeInfo and returns some minimal info. - """ - def cb(info): - discoInfo = disco.DiscoInfo() - for item in info: - discoInfo.append(item) - self.assertIn(('pubsub', 'service'), discoInfo.identities) - self.assertIn(disco.NS_DISCO_ITEMS, discoInfo.features) - - d = self.service.getDiscoInfo(JID('user@example.org/home'), - JID('pubsub.example.org'), '') - d.addCallback(cb) - return d - - - def test_publish(self): - """ - Non-overridden L{PubSubService.publish} yields unsupported error. - """ - - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('publish', result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_subscribe(self): - """ - Non-overridden L{PubSubService.subscribe} yields unsupported error. - """ - - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('subscribe', result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_unsubscribe(self): - """ - Non-overridden L{PubSubService.unsubscribe} yields unsupported error. - """ - - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('subscribe', result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_subscriptions(self): - """ - Non-overridden L{PubSubService.subscriptions} yields unsupported error. - """ - - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('retrieve-subscriptions', - result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_affiliations(self): - """ - Non-overridden L{PubSubService.affiliations} yields unsupported error. - """ - - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('retrieve-affiliations', - result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_create(self): - """ - Non-overridden L{PubSubService.create} yields unsupported error. - """ - - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('create-nodes', result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_getDefaultConfiguration(self): - """ - Non-overridden L{PubSubService.getDefaultConfiguration} yields - unsupported error. - """ - - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('retrieve-default', result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_getConfiguration(self): - """ - Non-overridden L{PubSubService.getConfiguration} yields unsupported - error. - """ - - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('config-node', result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_setConfiguration(self): - """ - Non-overridden L{PubSubService.setConfiguration} yields unsupported - error. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#node_config - - 0 - 1 - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('config-node', result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_setConfigurationOptionsDict(self): - """ - Options should be passed as a dictionary, not a form. - """ - - xml = """ - - - - - - http://jabber.org/protocol/pubsub#node_config - - 0 - 1 - - - - - """ - - def getConfigurationOptions(): - return { - "pubsub#persist_items": - {"type": "boolean", - "label": "Persist items to storage"}, - "pubsub#deliver_payloads": - {"type": "boolean", - "label": "Deliver payloads with event notifications"} - } - - def setConfiguration(requestor, service, nodeIdentifier, options): - self.assertIn('pubsub#deliver_payloads', options) - self.assertFalse(options['pubsub#deliver_payloads']) - self.assertIn('pubsub#persist_items', options) - self.assertTrue(options['pubsub#persist_items']) - - self.service.getConfigurationOptions = getConfigurationOptions - self.service.setConfiguration = setConfiguration - return self.handleRequest(xml) - - - def test_items(self): - """ - Non-overridden L{PubSubService.items} yields unsupported error. - """ - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('retrieve-items', result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_retract(self): - """ - Non-overridden L{PubSubService.retract} yields unsupported error. - """ - xml = """ - - - - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('retract-items', result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_purge(self): - """ - Non-overridden L{PubSubService.purge} yields unsupported error. - """ - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('purge-nodes', result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_delete(self): - """ - Non-overridden L{PubSubService.delete} yields unsupported error. - """ - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('delete-nodes', result.appCondition['feature']) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_unknown(self): - """ - Unknown verb yields unsupported error. - """ - xml = """ - - - - - - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - - d = self.handleRequest(xml) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - -class PubSubResourceTest(unittest.TestCase): - - def setUp(self): - self.resource = pubsub.PubSubResource() - - - def test_interface(self): - """ - Do instances of L{pubsub.PubSubResource} provide L{iwokkel.IPubSubResource}? - """ - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - - - def test_getNodes(self): - """ - Default getNodes returns an empty list. - """ - def cb(nodes): - self.assertEquals([], nodes) - - d = self.resource.getNodes(JID('user@example.org/home'), - JID('pubsub.example.org'), - '') - d.addCallback(cb) - return d - - - def test_publish(self): - """ - Non-overridden L{PubSubResource.publish} yields unsupported - error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('publish', result.appCondition['feature']) - - d = self.resource.publish(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_subscribe(self): - """ - Non-overridden subscriptions yields unsupported error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('subscribe', result.appCondition['feature']) - - d = self.resource.subscribe(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_unsubscribe(self): - """ - Non-overridden unsubscribe yields unsupported error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('subscribe', result.appCondition['feature']) - - d = self.resource.unsubscribe(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_subscriptions(self): - """ - Non-overridden subscriptions yields unsupported error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('retrieve-subscriptions', - result.appCondition['feature']) - - d = self.resource.subscriptions(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_affiliations(self): - """ - Non-overridden affiliations yields unsupported error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('retrieve-affiliations', - result.appCondition['feature']) - - d = self.resource.affiliations(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_create(self): - """ - Non-overridden create yields unsupported error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('create-nodes', result.appCondition['feature']) - - d = self.resource.create(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_default(self): - """ - Non-overridden default yields unsupported error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('retrieve-default', - result.appCondition['feature']) - - d = self.resource.default(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_configureGet(self): - """ - Non-overridden configureGet yields unsupported - error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('config-node', result.appCondition['feature']) - - d = self.resource.configureGet(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_configureSet(self): - """ - Non-overridden configureSet yields unsupported error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('config-node', result.appCondition['feature']) - - d = self.resource.configureSet(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_items(self): - """ - Non-overridden items yields unsupported error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('retrieve-items', result.appCondition['feature']) - - d = self.resource.items(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_retract(self): - """ - Non-overridden retract yields unsupported error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('retract-items', result.appCondition['feature']) - - d = self.resource.retract(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_purge(self): - """ - Non-overridden purge yields unsupported error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('purge-nodes', result.appCondition['feature']) - - d = self.resource.purge(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_delete(self): - """ - Non-overridden delete yields unsupported error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('delete-nodes', result.appCondition['feature']) - - d = self.resource.delete(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_affiliationsGet(self): - """ - Non-overridden owner affiliations get yields unsupported error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('modify-affiliations', - result.appCondition['feature']) - - d = self.resource.affiliationsGet(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d - - - def test_affiliationsSet(self): - """ - Non-overridden owner affiliations set yields unsupported error. - """ - - def cb(result): - self.assertEquals('feature-not-implemented', result.condition) - self.assertEquals('unsupported', result.appCondition.name) - self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) - self.assertEquals('modify-affiliations', - result.appCondition['feature']) - - d = self.resource.affiliationsSet(pubsub.PubSubRequest()) - self.assertFailure(d, error.StanzaError) - d.addCallback(cb) - return d diff -r 7430d1f6db22 -r c8cb4e867897 wokkel/test/test_rsm.py --- a/wokkel/test/test_rsm.py Wed Nov 01 22:34:51 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,662 +0,0 @@ -# Copyright (c) Adrien Cossa. -# See LICENSE for details. - -""" -Tests for L{wokkel.rsm}. -""" - -from zope.interface import verify - -from twisted.trial import unittest -from twisted.words.xish import domish -from twisted.words.protocols.jabber.jid import JID -from twisted.words.protocols.jabber.xmlstream import toResponse -from twisted.internet import defer - -from wokkel.generic import parseXml -from wokkel import iwokkel -from wokkel.test.helpers import XmlStreamStub, TestableRequestHandlerMixin - -from sat.tmp.wokkel import pubsub -from sat.tmp.wokkel.rsm import NS_RSM, RSMRequest, RSMResponse, PubSubClient, PubSubService - -import uuid - -RSMResponse.__eq__ = lambda self, other: self.first == other.first and\ - self.last == other.last and\ - self.index == other.index and\ - self.count == other.count - -class RSMRequestTest(unittest.TestCase): - """ - Tests for L{rsm.RSMRequest}. - """ - - def test___init__(self): - """ - Fail to initialize a RSMRequest with wrong attribute values. - """ - self.assertRaises(AssertionError, RSMRequest, index=371, after=u'test') - self.assertRaises(AssertionError, RSMRequest, index=371, before=u'test') - self.assertRaises(AssertionError, RSMRequest, before=117) - self.assertRaises(AssertionError, RSMRequest, after=312) - self.assertRaises(AssertionError, RSMRequest, after=u'117', before=u'312') - - def test_parse(self): - """ - Parse a request element asking for the first page. - """ - xml = """ - - Pete - - 1 - - - """ - request = RSMRequest.fromElement(parseXml(xml)) - self.assertEqual(1, request.max) - self.assertIdentical(None, request.index) - self.assertIdentical(None, request.after) - self.assertIdentical(None, request.before) - - def test_parseSecondPage(self): - """ - Parse a request element asking for a next page. - """ - xml = """ - - Pete - - 3 - peterpan@neverland.lit - - - """ - request = RSMRequest.fromElement(parseXml(xml)) - self.assertEqual(3, request.max) - self.assertIdentical(None, request.index) - self.assertEqual(u'peterpan@neverland.lit', request.after) - self.assertIdentical(None, request.before) - - def test_parsePreviousPage(self): - """ - Parse a request element asking for a previous page. - """ - xml = """ - - Pete - - 5 - peterpan@pixyland.org - - - """ - request = RSMRequest.fromElement(parseXml(xml)) - self.assertEqual(5, request.max) - self.assertIdentical(None, request.index) - self.assertIdentical(None, request.after) - self.assertEqual(u'peterpan@pixyland.org', request.before) - - def test_parseLastPage(self): - """ - Parse a request element asking for the last page. - """ - xml = """ - - Pete - - 7 - - - - """ - request = RSMRequest.fromElement(parseXml(xml)) - self.assertEqual(7, request.max) - self.assertIdentical(None, request.index) - self.assertIdentical(None, request.after) - self.assertEqual('', request.before) - - def test_parseOutOfOrderPage(self): - """ - Parse a request element asking for a page out of order. - """ - xml = """ - - Pete - - 9 - 371 - - - """ - request = RSMRequest.fromElement(parseXml(xml)) - self.assertEqual(9, request.max) - self.assertEqual(371, request.index) - self.assertIdentical(None, request.after) - self.assertIdentical(None, request.before) - - def test_parseItemCount(self): - """ - Parse a request element asking for the items count. - """ - xml = """ - - Pete - - 0 - - - """ - request = RSMRequest.fromElement(parseXml(xml)) - self.assertEqual(0, request.max) - self.assertIdentical(None, request.index) - self.assertIdentical(None, request.after) - self.assertIdentical(None, request.before) - - def test_render(self): - """ - Embed a page request in the element. - """ - element = domish.Element(('jabber:iq:search', 'query')) - element.addElement('items')['max_items'] = u'10' - RSMRequest(1).render(element) - - self.assertEqual(u'10', element.items['max_items']) # not changed - - self.assertEqual(NS_RSM, element.set.uri) - self.assertEqual(u'1', ''.join(element.set.max.children)) - self.assertIdentical(None, element.set.after) - self.assertIdentical(None, element.set.before) - self.assertIdentical(None, element.set.index) - - def test_renderPubSub(self): - """ - Embed a page request in the pubsub element. - """ - element = domish.Element((pubsub.NS_PUBSUB, 'pubsub')) - element.addElement('items')['max_items'] = u'10' - RSMRequest(3).render(element) - - self.assertEqual(u'10', element.items['max_items']) # not changed - - self.assertEqual(NS_RSM, element.set.uri) - self.assertEqual(u'3', ''.join(element.set.max.children)) - self.assertIdentical(None, element.set.after) - self.assertIdentical(None, element.set.before) - self.assertIdentical(None, element.set.index) - - def test_renderItems(self): - """ - Embed a page request in the element, specify items. - """ - element = domish.Element(('jabber:iq:search', 'query')) - RSMRequest(5, index=127).render(element) - self.assertEqual(NS_RSM, element.set.uri) - self.assertEqual(u'5', ''.join(element.set.max.children)) - self.assertIdentical(None, element.set.after) - self.assertIdentical(None, element.set.before) - self.assertEqual(u'127', ''.join(element.set.index.children)) - - def test_renderAfter(self): - """ - Embed a page request in the element, specify after. - """ - element = domish.Element(('jabber:iq:search', 'query')) - RSMRequest(5, after=u'test').render(element) - self.assertEqual(NS_RSM, element.set.uri) - self.assertEqual(u'5', ''.join(element.set.max.children)) - self.assertEqual(u'test', ''.join(element.set.after.children)) - self.assertIdentical(None, element.set.before) - self.assertIdentical(None, element.set.index) - - def test_renderBefore(self): - """ - Embed a page request in the element, specify before. - """ - element = domish.Element(('jabber:iq:search', 'query')) - RSMRequest(5, before=u'test').render(element) - self.assertEqual(NS_RSM, element.set.uri) - self.assertEqual(u'5', ''.join(element.set.max.children)) - self.assertIdentical(None, element.set.after) - self.assertEqual(u'test', ''.join(element.set.before.children)) - self.assertIdentical(None, element.set.index) - - -class RSMResponseTest(unittest.TestCase): - """ - Tests for L{rsm.RSMResponse}. - """ - - def test___init__(self): - """ - Fail to initialize a RSMResponse with wrong attribute values. - """ - self.assertRaises(AssertionError, RSMResponse, index=127, first=u'127') - self.assertRaises(AssertionError, RSMResponse, index=127, last=u'351') - - def test_parse(self): - """ - Parse a response element returning a page. - """ - xml = """ - - - stpeter@jabber.org - peterpan@neverland.lit - 800 - - - """ - response = RSMResponse.fromElement(parseXml(xml)) - self.assertEqual(800, response.count) - self.assertEqual(20, response.index) - self.assertEqual(u'stpeter@jabber.org', response.first) - self.assertEqual(u'peterpan@neverland.lit', response.last) - - def test_parseEmptySet(self): - """ - Parse a response element returning an empty set. - """ - xml = """ - - - 800 - - - """ - response = RSMResponse.fromElement(parseXml(xml)) - self.assertEqual(800, response.count) - self.assertIdentical(None, response.first) - self.assertIdentical(None, response.last) - self.assertIdentical(None, response.index) - - def test_render(self): - """ - Embed a page response in the element. - """ - element = domish.Element(('jabber:iq:search', 'query')) - RSMResponse(u'stpeter@jabber.org', u'peterpan@neverland.lit', 20, 800).render(element) - - self.assertEqual(NS_RSM, element.set.uri) - self.assertEqual(u'800', ''.join(element.set.count.children)) - self.assertEqual(u'stpeter@jabber.org', - ''.join(element.set.first.children)) - self.assertEqual(u'peterpan@neverland.lit', - ''.join(element.set.last.children)) - self.assertEqual(u'20', element.set.first['index']) - - def test_renderEmptySet(self): - """ - Embed a page response in the element, for empty set. - """ - element = domish.Element(('jabber:iq:search', 'query')) - RSMResponse(count=800).render(element) - - self.assertEqual(NS_RSM, element.set.uri) - self.assertEqual(u'800', ''.join(element.set.count.children)) - self.assertIdentical(None, element.set.first) - self.assertIdentical(None, element.set.last) - - -class PubSubClientTest(unittest.TestCase): - """ - Tests for L{rsm.PubSubClient}. - """ - timeout = 2 - - def setUp(self): - self.stub = XmlStreamStub() - self.protocol = PubSubClient() - self.protocol.xmlstream = self.stub.xmlstream - self.protocol.connectionInitialized() - - def test_items(self): - """ - Test sending items request to get the first page. - """ - def cb(response): - items, rsm = response - self.assertEquals(2, len(items)) - self.assertEquals([item1, item2], items) - self.assertEquals(rsm, RSMResponse('item1', 'item2', 0, 800)) - - d = self.protocol.items(JID('pubsub.example.org'), 'test', - rsm_request=RSMRequest(2)) - d.addCallback(cb) - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('get', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(pubsub.NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'items', pubsub.NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - - set_elts = list(domish.generateElementsQNamed(iq.pubsub.children, - 'set', NS_RSM)) - self.assertEquals(1, len(set_elts)) - set_elt = set_elts[0] - self.assertEquals(u'2', ''.join(set_elt.max.children)) - - response = toResponse(iq, 'result') - items = response.addElement((pubsub.NS_PUBSUB, - 'pubsub')).addElement('items') - items['node'] = 'test' - item1 = items.addElement('item') - item1['id'] = 'item1' - item2 = items.addElement('item') - item2['id'] = 'item2' - RSMResponse(u'item1', u'item2', 0, 800).render(response.pubsub) - self.stub.send(response) - - return d - - def test_itemsAfter(self): - """ - Test sending items request to get the next page. - """ - def cb(response): - items, rsm = response - self.assertEquals(2, len(items)) - self.assertEquals([item1, item2], items) - self.assertEquals(rsm, RSMResponse('item3', 'item4', 2, 800)) - - d = self.protocol.items(JID('pubsub.example.org'), 'test', - rsm_request=RSMRequest(2, after=u'item2')) - d.addCallback(cb) - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('get', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(pubsub.NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'items', pubsub.NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - - set_elts = list(domish.generateElementsQNamed(iq.pubsub.children, - 'set', NS_RSM)) - self.assertEquals(1, len(set_elts)) - set_elt = set_elts[0] - self.assertEquals(u'2', ''.join(set_elt.max.children)) - self.assertEquals(u'item2', ''.join(set_elt.after.children)) - - response = toResponse(iq, 'result') - items = response.addElement((pubsub.NS_PUBSUB, - 'pubsub')).addElement('items') - items['node'] = 'test' - item1 = items.addElement('item') - item1['id'] = 'item3' - item2 = items.addElement('item') - item2['id'] = 'item4' - RSMResponse(u'item3', u'item4', 2, 800).render(response.pubsub) - self.stub.send(response) - - return d - - def test_itemsBefore(self): - """ - Test sending items request to get the previous page. - """ - def cb(response): - items, rsm = response - self.assertEquals(2, len(items)) - self.assertEquals([item1, item2], items) - self.assertEquals(rsm, RSMResponse('item1', 'item2', 0, 800)) - - d = self.protocol.items(JID('pubsub.example.org'), 'test', - rsm_request=RSMRequest(2, before=u'item3')) - d.addCallback(cb) - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('get', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(pubsub.NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'items', pubsub.NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - - set_elts = list(domish.generateElementsQNamed(iq.pubsub.children, - 'set', NS_RSM)) - self.assertEquals(1, len(set_elts)) - set_elt = set_elts[0] - self.assertEquals(u'2', ''.join(set_elt.max.children)) - self.assertEquals(u'item3', ''.join(set_elt.before.children)) - - response = toResponse(iq, 'result') - items = response.addElement((pubsub.NS_PUBSUB, - 'pubsub')).addElement('items') - items['node'] = 'test' - item1 = items.addElement('item') - item1['id'] = 'item1' - item2 = items.addElement('item') - item2['id'] = 'item2' - RSMResponse(u'item1', u'item2', 0, 800).render(response.pubsub) - self.stub.send(response) - - return d - - def test_itemsIndex(self): - """ - Test sending items request to get a page out of order. - """ - def cb(response): - items, rsm = response - self.assertEquals(3, len(items)) - self.assertEquals([item1, item2, item3], items) - self.assertEquals(rsm, RSMResponse('item4', 'item6', 3, 800)) - - d = self.protocol.items(JID('pubsub.example.org'), 'test', - rsm_request=RSMRequest(3, index=3)) - d.addCallback(cb) - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('get', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(pubsub.NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'items', pubsub.NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - - set_elts = list(domish.generateElementsQNamed(iq.pubsub.children, - 'set', NS_RSM)) - self.assertEquals(1, len(set_elts)) - set_elt = set_elts[0] - self.assertEquals(u'3', ''.join(set_elt.max.children)) - self.assertEquals(u'3', ''.join(set_elt.index.children)) - - response = toResponse(iq, 'result') - items = response.addElement((pubsub.NS_PUBSUB, - 'pubsub')).addElement('items') - items['node'] = 'test' - item1 = items.addElement('item') - item1['id'] = 'item4' - item2 = items.addElement('item') - item2['id'] = 'item5' - item3 = items.addElement('item') - item3['id'] = 'item6' - RSMResponse(u'item4', u'item6', 3, 800).render(response.pubsub) - self.stub.send(response) - - return d - - def test_itemsCount(self): - """ - Test sending items request to count them. - """ - def cb(response): - items, rsm = response - self.assertEquals(0, len(items)) - self.assertEquals(rsm, RSMResponse(count=800)) - - d = self.protocol.items(JID('pubsub.example.org'), 'test', - rsm_request=RSMRequest(0)) - d.addCallback(cb) - - iq = self.stub.output[-1] - self.assertEquals('pubsub.example.org', iq.getAttribute('to')) - self.assertEquals('get', iq.getAttribute('type')) - self.assertEquals('pubsub', iq.pubsub.name) - self.assertEquals(pubsub.NS_PUBSUB, iq.pubsub.uri) - children = list(domish.generateElementsQNamed(iq.pubsub.children, - 'items', pubsub.NS_PUBSUB)) - self.assertEquals(1, len(children)) - child = children[0] - self.assertEquals('test', child['node']) - - set_elts = list(domish.generateElementsQNamed(iq.pubsub.children, - 'set', NS_RSM)) - self.assertEquals(1, len(set_elts)) - set_elt = set_elts[0] - self.assertEquals(u'0', ''.join(set_elt.max.children)) - - response = toResponse(iq, 'result') - response.addElement((pubsub.NS_PUBSUB, 'pubsub')) - RSMResponse(count=800).render(response.pubsub) - self.stub.send(response) - - return d - - -class PubSubServiceTest(unittest.TestCase, TestableRequestHandlerMixin): - - def setUp(self): - self.stub = XmlStreamStub() - self.resource = pubsub.PubSubResource() - self.service = PubSubService(self.resource) - self.service.send = self.stub.xmlstream.send - - def test_on_items(self): - """ - On a items request, return the first item for the given node. - """ - xml = """ - - - - - - 1 - - - """ - - def items(request): - rsm = RSMResponse(u'item', u'item', 0, 800).toElement() - return defer.succeed([pubsub.Item('current'), rsm]) - - def cb(element): - self.assertEqual(pubsub.NS_PUBSUB, element.uri) - self.assertEqual(pubsub.NS_PUBSUB, element.items.uri) - self.assertEqual(1, len(element.items.children)) - item = element.items.children[-1] - self.assertTrue(domish.IElement.providedBy(item)) - self.assertEqual('item', item.name) - self.assertEqual(pubsub.NS_PUBSUB, item.uri) - self.assertEqual('current', item['id']) - self.assertEqual(NS_RSM, element.set.uri) - self.assertEqual('800', ''.join(element.set.count.children)) - self.assertEqual('0', element.set.first['index']) - self.assertEqual('item', ''.join(element.set.first.children)) - self.assertEqual('item', ''.join(element.set.last.children)) - - self.resource.items = items - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - def test_on_itemsIndex(self): - """ - On a items request, return some items out of order for the given node. - """ - xml = """ - - - - - - 2 - 3 - - - """ - - def items(request): - rsm = RSMResponse(u'i1', u'i2', 3, 800).toElement() - return defer.succeed([pubsub.Item('i1'), pubsub.Item('i2'), rsm]) - - def cb(element): - self.assertEqual(pubsub.NS_PUBSUB, element.uri) - self.assertEqual(pubsub.NS_PUBSUB, element.items.uri) - self.assertEqual(2, len(element.items.children)) - item = element.items.children[0] - self.assertTrue(domish.IElement.providedBy(item)) - self.assertEqual('item', item.name) - self.assertEqual(pubsub.NS_PUBSUB, item.uri) - self.assertEqual('i1', item['id']) - item = element.items.children[1] - self.assertTrue(domish.IElement.providedBy(item)) - self.assertEqual('item', item.name) - self.assertEqual(pubsub.NS_PUBSUB, item.uri) - self.assertEqual('i2', item['id']) - self.assertEqual(NS_RSM, element.set.uri) - self.assertEqual('800', ''.join(element.set.count.children)) - self.assertEqual('3', element.set.first['index']) - self.assertEqual('i1', ''.join(element.set.first.children)) - self.assertEqual('i2', ''.join(element.set.last.children)) - - self.resource.items = items - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d - - def test_on_itemsCount(self): - """ - On a items request, return the items count. - """ - xml = """ - - - - - - 0 - - - """ - - def items(request): - rsm = RSMResponse(count=800).toElement() - return defer.succeed([rsm]) - - def cb(element): - self.assertEqual(pubsub.NS_PUBSUB, element.uri) - self.assertEqual(pubsub.NS_PUBSUB, element.items.uri) - self.assertEqual(0, len(element.items.children)) - self.assertEqual(NS_RSM, element.set.uri) - self.assertEqual('800', ''.join(element.set.count.children)) - self.assertEqual(None, element.set.first) - self.assertEqual(None, element.set.last) - - self.resource.items = items - verify.verifyObject(iwokkel.IPubSubResource, self.resource) - d = self.handleRequest(xml) - d.addCallback(cb) - return d