Add PubSubRequest that parses requests and replaces separate parameters. The publish-subscribe protocol has a large number of optional features. Adding support for a feature often means more information needs to be passed, and required changing the signature of the methods that need to be overridden by adaptors. This change adds the new PubSubRequest class that features a fromElement method that creates a new instance, parses a request and then fills the instance attributes where appropriate. This instance is then the only argument that is passed to the corresponding handler methods. diff -r 0bfc0b2a633c wokkel/generic.py --- a/wokkel/generic.py Wed Apr 01 17:25:11 2009 +0200 +++ b/wokkel/generic.py Wed Apr 01 17:26:46 2009 +0200 @@ -10,7 +10,7 @@ from zope.interface import implements from twisted.internet import defer, protocol -from twisted.words.protocols.jabber import error, xmlstream +from twisted.words.protocols.jabber import error, jid, xmlstream from twisted.words.protocols.jabber.xmlstream import toResponse from twisted.words.xish import domish, utility @@ -162,6 +162,37 @@ self.sink.send = lambda obj: self.source.dispatch(obj) + +class Stanza(object): + """ + Abstract representation of a stanza. + + @ivar sender: The sending entity. + @type sender: L{jid.JID} + @ivar recipient: The receiving entity. + @type recipient: L{jid.JID} + """ + + sender = None + recipient = None + stanzaType = None + + @classmethod + def fromElement(Class, element): + stanza = Class() + stanza.parseElement(element) + return stanza + + + def parseElement(self, element): + self.sender = jid.internJID(element['from']) + if element.hasAttribute('from'): + self.sender = jid.internJID(element['from']) + if element.hasAttribute('to'): + self.recipient = jid.internJID(element['to']) + self.stanzaType = element.getAttribute('type') + + class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory): protocol = xmlstream.XmlStream diff -r 0bfc0b2a633c wokkel/iwokkel.py --- a/wokkel/iwokkel.py Wed Apr 01 17:25:11 2009 +0200 +++ b/wokkel/iwokkel.py Wed Apr 01 17:26:46 2009 +0200 @@ -278,6 +278,7 @@ C{list} of L{domish.Element}) """ + def notifyDelete(service, nodeIdentifier, subscribers, redirectURI=None): """ @@ -295,77 +296,60 @@ @type redirectURI: C{str} """ - def publish(requestor, service, nodeIdentifier, items): + + def publish(request): """ Called when a publish request has been received. - @param requestor: The entity the request originated from. - @type requestor: L{jid.JID} - @param service: The entity the request was addressed to. - @type service: L{jid.JID} - @param nodeIdentifier: The identifier of the node to publish to. - @type nodeIdentifier: C{unicode} - @param items: The items to be published as L{domish} elements. - @type items: C{list} of C{domish.Element} + @param request: The publish-subscribe request. + @type request: L{wokkel.pubsub.PubSubRequest} @return: deferred that fires on success. @rtype: L{defer.Deferred} """ - def subscribe(requestor, service, nodeIdentifier, subscriber): + + def subscribe(request): """ Called when a subscribe request has been received. - @param requestor: The entity the request originated from. - @type requestor: L{jid.JID} - @param service: The entity the request was addressed to. - @type service: L{jid.JID} - @param nodeIdentifier: The identifier of the node to subscribe to. - @type nodeIdentifier: C{unicode} - @param subscriber: The entity to be subscribed. - @type subscriber: L{jid.JID} + @param request: The publish-subscribe request. + @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a L{Subscription}. @rtype: L{defer.Deferred} """ - def unsubscribe(requestor, service, nodeIdentifier, subscriber): + + def unsubscribe(request): """ Called when a subscribe request has been received. - @param requestor: The entity the request originated from. - @type requestor: L{jid.JID} - @param service: The entity the request was addressed to. - @type service: L{jid.JID} - @param nodeIdentifier: The identifier of the node to unsubscribe from. - @type nodeIdentifier: C{unicode} - @param subscriber: The entity to be unsubscribed. - @type subscriber: L{jid.JID} + @param request: The publish-subscribe request. + @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with C{None} when unsubscription has succeeded. @rtype: L{defer.Deferred} """ - def subscriptions(requestor, service): + + def subscriptions(request): """ Called when a subscriptions retrieval request has been received. - @param requestor: The entity the request originated from. - @type requestor: L{jid.JID} - @param service: The entity the request was addressed to. - @type service: L{jid.JID} + @param request: The publish-subscribe request. + @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a C{list} of subscriptions as L{Subscription}. @rtype: L{defer.Deferred} """ - def affiliations(requestor, service): + + def affiliations(request): """ Called when a affiliations retrieval request has been received. - @param requestor: The entity the request originated from. - @type requestor: L{jid.JID} - @param service: The entity the request was addressed to. - @type service: L{jid.JID} + @param request: The publish-subscribe request. + @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a C{list} of affiliations as C{tuple}s of (node identifier as C{unicode}, affiliation state as C{str}). The affiliation can be C{'owner'}, C{'publisher'}, @@ -373,24 +357,19 @@ @rtype: L{defer.Deferred} """ - def create(requestor, service, nodeIdentifier): + + def create(request): """ Called when a node creation request has been received. - @param requestor: The entity the request originated from. - @type requestor: L{jid.JID} - @param service: The entity the request was addressed to. - @type service: L{jid.JID} - @param nodeIdentifier: The suggestion for the identifier of the node to - be created. If the request did not include a - suggestion for the node identifier, the value - is C{None}. - @type nodeIdentifier: C{unicode} or C{NoneType} + @param request: The publish-subscribe request. + @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a C{unicode} that represents the identifier of the new node. @rtype: L{defer.Deferred} """ + def getConfigurationOptions(): """ Retrieve all known node configuration options. @@ -426,17 +405,13 @@ @rtype: C{dict}. """ - def getDefaultConfiguration(requestor, service, nodeType): + + def getDefaultConfiguration(request): """ Called when a default node configuration request has been received. - @param requestor: The entity the request originated from. - @type requestor: L{jid.JID} - @param service: The entity the request was addressed to. - @type service: L{jid.JID} - @param nodeType: The type of node for which the configuration is - retrieved, C{'leaf'} or C{'collection'}. - @type nodeType: C{str} + @param request: The publish-subscribe request. + @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a C{dict} representing the default node configuration. Keys are C{str}s that represent the field name. Values can be of types C{unicode}, C{int} or @@ -444,85 +419,74 @@ @rtype: L{defer.Deferred} """ - def getConfiguration(requestor, service, nodeIdentifier): + + def getConfiguration(request): """ Called when a node configuration retrieval request has been received. - @param requestor: The entity the request originated from. - @type requestor: L{jid.JID} - @param service: The entity the request was addressed to. - @type service: L{jid.JID} - @param nodeIdentifier: The identifier of the node to retrieve the - configuration from. - @type nodeIdentifier: C{unicode} + @param request: The publish-subscribe request. + @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a C{dict} representing the node configuration. Keys are C{str}s that represent the field name. Values can be of types C{unicode}, C{int} or C{bool}. @rtype: L{defer.Deferred} """ - def setConfiguration(requestor, service, nodeIdentifier, options): + + def setConfiguration(request): """ Called when a node configuration change request has been received. - @param requestor: The entity the request originated from. - @type requestor: L{jid.JID} - @param service: The entity the request was addressed to. - @type service: L{jid.JID} - @param nodeIdentifier: The identifier of the node to change the - configuration of. - @type nodeIdentifier: C{unicode} + @param request: The publish-subscribe request. + @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with C{None} when the node's configuration has been changed. @rtype: L{defer.Deferred} """ - def items(requestor, service, nodeIdentifier, maxItems, itemIdentifiers): + + def items(request): """ Called when a items retrieval request has been received. - @param requestor: The entity the request originated from. - @type requestor: L{jid.JID} - @param service: The entity the request was addressed to. - @type service: L{jid.JID} - @param nodeIdentifier: The identifier of the node to retrieve items - from. - @type nodeIdentifier: C{unicode} + @param request: The publish-subscribe request. + @type request: L{wokkel.pubsub.PubSubRequest} + @return: A deferred that fires with a C{list} of L{pubsub.Item}. + @rtype: L{defer.Deferred} """ - def retract(requestor, service, nodeIdentifier, itemIdentifiers): + + def retract(request): """ Called when a item retraction request has been received. - @param requestor: The entity the request originated from. - @type requestor: L{jid.JID} - @param service: The entity the request was addressed to. - @type service: L{jid.JID} - @param nodeIdentifier: The identifier of the node to retract items - from. - @type nodeIdentifier: C{unicode} + @param request: The publish-subscribe request. + @type request: L{wokkel.pubsub.PubSubRequest} + @return: A deferred that fires with C{None} when the given items have + been retracted. + @rtype: L{defer.Deferred} """ - def purge(requestor, service, nodeIdentifier): + + def purge(request): """ Called when a node purge request has been received. - @param requestor: The entity the request originated from. - @type requestor: L{jid.JID} - @param service: The entity the request was addressed to. - @type service: L{jid.JID} - @param nodeIdentifier: The identifier of the node to be purged. - @type nodeIdentifier: C{unicode} + @param request: The publish-subscribe request. + @type request: L{wokkel.pubsub.PubSubRequest} + @return: A deferred that fires with C{None} when the node has been + purged. + @rtype: L{defer.Deferred} """ - def delete(requestor, service, nodeIdentifier): + + def delete(request): """ Called when a node deletion request has been received. - @param requestor: The entity the request originated from. - @type requestor: L{jid.JID} - @param service: The entity the request was addressed to. - @type service: L{jid.JID} - @param nodeIdentifier: The identifier of the node to be delete. - @type nodeIdentifier: C{unicode} + @param request: The publish-subscribe request. + @type request: L{wokkel.pubsub.PubSubRequest} + @return: A deferred that fires with C{None} when the node has been + deleted. + @rtype: L{defer.Deferred} """ diff -r 0bfc0b2a633c wokkel/pubsub.py --- a/wokkel/pubsub.py Wed Apr 01 17:25:11 2009 +0200 +++ b/wokkel/pubsub.py Wed Apr 01 17:26:46 2009 +0200 @@ -16,7 +16,7 @@ from twisted.words.protocols.jabber import jid, error, xmlstream from twisted.words.xish import domish -from wokkel import disco, data_form, shim +from wokkel import disco, data_form, generic, shim from wokkel.subprotocols import IQHandlerMixin, XMPPHandler from wokkel.iwokkel import IPubSubClient, IPubSubService @@ -31,43 +31,12 @@ 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" -# In publish-subscribe namespace XPath query selector. -IN_NS_PUBSUB = '[@xmlns="' + NS_PUBSUB + '"]' -IN_NS_PUBSUB_OWNER = '[@xmlns="' + NS_PUBSUB_OWNER + '"]' - -# Publish-subscribe XPath queries -PUBSUB_ELEMENT = '/pubsub' + IN_NS_PUBSUB -PUBSUB_OWNER_ELEMENT = '/pubsub' + IN_NS_PUBSUB_OWNER -PUBSUB_GET = IQ_GET + PUBSUB_ELEMENT -PUBSUB_SET = IQ_SET + PUBSUB_ELEMENT -PUBSUB_OWNER_GET = IQ_GET + PUBSUB_OWNER_ELEMENT -PUBSUB_OWNER_SET = IQ_SET + PUBSUB_OWNER_ELEMENT - -# Publish-subscribe command XPath queries -PUBSUB_PUBLISH = PUBSUB_SET + '/publish' + IN_NS_PUBSUB -PUBSUB_CREATE = PUBSUB_SET + '/create' + IN_NS_PUBSUB -PUBSUB_SUBSCRIBE = PUBSUB_SET + '/subscribe' + IN_NS_PUBSUB -PUBSUB_UNSUBSCRIBE = PUBSUB_SET + '/unsubscribe' + IN_NS_PUBSUB -PUBSUB_OPTIONS_GET = PUBSUB_GET + '/options' + IN_NS_PUBSUB -PUBSUB_OPTIONS_SET = PUBSUB_SET + '/options' + IN_NS_PUBSUB -PUBSUB_DEFAULT = PUBSUB_OWNER_GET + '/default' + IN_NS_PUBSUB_OWNER -PUBSUB_CONFIGURE_GET = PUBSUB_OWNER_GET + '/configure' + IN_NS_PUBSUB_OWNER -PUBSUB_CONFIGURE_SET = PUBSUB_OWNER_SET + '/configure' + IN_NS_PUBSUB_OWNER -PUBSUB_SUBSCRIPTIONS = PUBSUB_GET + '/subscriptions' + IN_NS_PUBSUB -PUBSUB_AFFILIATIONS = PUBSUB_GET + '/affiliations' + IN_NS_PUBSUB -PUBSUB_AFFILIATIONS_GET = PUBSUB_OWNER_GET + '/affiliations' + \ - IN_NS_PUBSUB_OWNER -PUBSUB_AFFILIATIONS_SET = PUBSUB_OWNER_SET + '/affiliations' + \ - IN_NS_PUBSUB_OWNER -PUBSUB_SUBSCRIPTIONS_GET = PUBSUB_OWNER_GET + '/subscriptions' + \ - IN_NS_PUBSUB_OWNER -PUBSUB_SUBSCRIPTIONS_SET = PUBSUB_OWNER_SET + '/subscriptions' + \ - IN_NS_PUBSUB_OWNER -PUBSUB_ITEMS = PUBSUB_GET + '/items' + IN_NS_PUBSUB -PUBSUB_RETRACT = PUBSUB_SET + '/retract' + IN_NS_PUBSUB -PUBSUB_PURGE = PUBSUB_OWNER_SET + '/purge' + IN_NS_PUBSUB_OWNER -PUBSUB_DELETE = PUBSUB_OWNER_SET + '/delete' + IN_NS_PUBSUB_OWNER +# XPath to match pubsub requests +PUBSUB_REQUEST = '/iq[@type="get" or @type="set"]/' + \ + 'pubsub[@xmlns="' + NS_PUBSUB + '" or ' + \ + '@xmlns="' + NS_PUBSUB_OWNER + '"]' class SubscriptionPending(Exception): """ @@ -98,12 +67,18 @@ -class BadRequest(PubSubError): +class BadRequest(error.StanzaError): """ Bad request stanza error. """ def __init__(self, pubsubCondition=None, text=None): - PubSubError.__init__(self, 'bad-request', pubsubCondition, text) + if pubsubCondition: + appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition)) + else: + appCondition = None + error.StanzaError.__init__(self, 'bad-request', + text=text, + appCondition=appCondition) @@ -167,40 +142,362 @@ -class _PubSubRequest(xmlstream.IQ): +class PubSubRequest(generic.Stanza): """ - Publish subscribe request. + A publish-subscribe request. - @ivar verb: Request verb - @type verb: C{str} - @ivar namespace: Request namespace. - @type namespace: C{str} - @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'} - @type method: C{str} - @ivar command: Command element of the request. This is the direct child of - the C{pubsub} element in the C{namespace} with the name - C{verb}. + 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 L{_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} """ - def __init__(self, xs, verb, namespace=NS_PUBSUB, method='set'): - xmlstream.IQ.__init__(self, xs, method) - self.addElement((namespace, 'pubsub')) + verb = None - self.command = self.pubsub.addElement(verb) + affiliations = None + items = None + itemIdentifiers = None + maxItems = None + nodeIdentifier = None + nodeType = None + options = None + subscriber = None + subscriptionIdentifier = None + subscriptions = 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', + } - def send(self, to): + # 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'], + 'unsubscribe': ['nodeOrEmpty', 'jid'], + 'optionsGet': ['nodeOrEmpty', 'jid'], + 'optionsSet': ['nodeOrEmpty', 'jid', 'options'], + 'subscriptions': [], + 'affiliations': [], + 'create': ['nodeOrNone'], + 'default': ['default'], + 'configureGet': ['nodeOrEmpty'], + 'configureSet': ['nodeOrEmpty', 'configure'], + 'items': ['node', 'maxItems', 'itemIdentifiers'], + 'retract': ['node', 'itemIdentifiers'], + 'purge': ['node'], + 'delete': ['node'], + 'affiliationsGet': [], + 'affiliationsSet': [], + 'subscriptionsGet': [], + 'subscriptionsSet': [], + } + + def __init__(self, verb=None): + self.verb = verb + + + @staticmethod + def _findForm(element, formNamespace): """ - Send out request. + Find a Data Form. - Extends L{xmlstream.IQ.send} by requiring the C{to} parameter to be - a L{JID} instance. + Look for an element that represents a Data Form with the specified + form namespace as a child element of the given element. + """ + if not element: + return None - @param to: Entity to send the request to. - @type to: L{JID} + form = None + for child in element.elements(): + try: + form = data_form.Form.fromElement(child) + except data_form.Error: + continue + + if form.formNamespace != NS_PUBSUB_NODE_CONFIG: + continue + + return form + + + def _parse_node(self, verbElement): """ - destination = to.full() - return xmlstream.IQ.send(self, destination) + 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: + 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 = PubSubRequest._findForm(verbElement, NS_PUBSUB_NODE_CONFIG) + if form 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 = PubSubRequest._findForm(verbElement, NS_PUBSUB_NODE_CONFIG) + if form: + if form.formType == 'submit': + self.options = form.getValues() + elif form.formType == 'cancel': + self.options = {} + else: + raise BadRequest(text="Unexpected form type %r" % form.formType) + else: + raise BadRequest(text="Missing configuration form") + + + + 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): + """ + Parse maximum items into an items request. + """ + if self.maxItems: + verbElement['max_items'] = unicode(self.maxItems) + + + def _parse_options(self, verbElement): + form = PubSubRequest._findForm(verbElement, NS_PUBSUB_SUBSCRIBE_OPTIONS) + if form: + if form.formType == 'submit': + self.options = form.getValues() + elif form.formType == 'cancel': + self.options = {} + else: + raise BadRequest(text="Unexpected form type %r" % form.formType) + else: + raise BadRequest(text="Missing options form") + + def parseElement(self, element): + """ + Parse the publish-subscribe verb and parameters out of a request. + """ + generic.Stanza.parseElement(self, element) + + for child in element.pubsub.elements(): + key = (self.stanzaType, child.uri, child.name) + try: + verb = self._requestVerbMap[key] + except KeyError: + continue + else: + self.verb = verb + break + + if not self.verb: + raise NotImplementedError() + + for parameter in self._parameters[verb]: + getattr(self, '_parse_%s' % parameter)(child) + + + def send(self, xs): + """ + Send this request to its recipient. + + This renders all of the relevant parameters for this specific + requests into an L{xmlstream.IQ}, and invoke its C{send} method. + This returns a deferred that fires upon reception of a response. See + L{xmlstream.IQ} for details. + + @param xs: The XML stream to send the request on. + @type xs: L{xmlstream.XmlStream} + @rtype: L{defer.Deferred}. + """ + + try: + (self.stanzaType, + childURI, + childName) = self._verbRequestMap[self.verb] + except KeyError: + raise NotImplementedError() + + iq = xmlstream.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() @@ -336,11 +633,9 @@ @param nodeIdentifier: Optional suggestion for the id of the node. @type nodeIdentifier: C{unicode} """ - - - request = _PubSubRequest(self.xmlstream, 'create') - if nodeIdentifier: - request.command['node'] = nodeIdentifier + request = PubSubRequest('create') + request.recipient = service + request.nodeIdentifier = nodeIdentifier def cb(iq): try: @@ -350,7 +645,9 @@ new_node = nodeIdentifier return new_node - return request.send(service).addCallback(cb) + d = request.send(self.xmlstream) + d.addCallback(cb) + return d def deleteNode(self, service, nodeIdentifier): @@ -362,9 +659,10 @@ @param nodeIdentifier: The identifier of the node. @type nodeIdentifier: C{unicode} """ - request = _PubSubRequest(self.xmlstream, 'delete', NS_PUBSUB_OWNER) - request.command['node'] = nodeIdentifier - return request.send(service) + request = PubSubRequest('delete') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + return request.send(self.xmlstream) def subscribe(self, service, nodeIdentifier, subscriber): @@ -379,10 +677,10 @@ will get notifications of new published items. @type subscriber: L{JID} """ - request = _PubSubRequest(self.xmlstream, 'subscribe') - if nodeIdentifier: - request.command['node'] = nodeIdentifier - request.command['jid'] = subscriber.full() + request = PubSubRequest('subscribe') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + request.subscriber = subscriber def cb(iq): subscription = iq.pubsub.subscription["subscription"] @@ -397,7 +695,9 @@ # yielded a stanza error. return None - return request.send(service).addCallback(cb) + d = request.send(self.xmlstream) + d.addCallback(cb) + return d def unsubscribe(self, service, nodeIdentifier, subscriber): @@ -411,11 +711,11 @@ @param subscriber: The entity to unsubscribe from the node. @type subscriber: L{JID} """ - request = _PubSubRequest(self.xmlstream, 'unsubscribe') - if nodeIdentifier: - request.command['node'] = nodeIdentifier - request.command['jid'] = subscriber.full() - return request.send(service) + request = PubSubRequest('unsubscribe') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + request.subscriber = subscriber + return request.send(self.xmlstream) def publish(self, service, nodeIdentifier, items=None): @@ -429,13 +729,11 @@ @param items: Optional list of L{Item}s to publish. @type items: C{list} """ - request = _PubSubRequest(self.xmlstream, 'publish') - request.command['node'] = nodeIdentifier - if items: - for item in items: - request.command.addChild(item) - - return request.send(service) + request = PubSubRequest('publish') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + request.items = items + return request.send(self.xmlstream) def items(self, service, nodeIdentifier, maxItems=None): @@ -449,11 +747,11 @@ @param maxItems: Optional limit on the number of retrieved items. @type maxItems: C{int} """ - request = _PubSubRequest(self.xmlstream, 'items', method='get') - if nodeIdentifier: - request.command['node'] = nodeIdentifier + request = PubSubRequest('items') + request.recipient = service + request.nodeIdentifier = nodeIdentifier if maxItems: - request.command["max_items"] = str(int(maxItems)) + request.maxItems = str(int(maxItems)) def cb(iq): items = [] @@ -462,7 +760,9 @@ items.append(element) return items - return request.send(service).addCallback(cb) + d = request.send(self.xmlstream) + d.addCallback(cb) + return d @@ -497,27 +797,7 @@ implements(IPubSubService) iqHandlers = { - PUBSUB_PUBLISH: '_onPublish', - PUBSUB_CREATE: '_onCreate', - PUBSUB_SUBSCRIBE: '_onSubscribe', - PUBSUB_OPTIONS_GET: '_onOptionsGet', - PUBSUB_OPTIONS_SET: '_onOptionsSet', - PUBSUB_AFFILIATIONS: '_onAffiliations', - PUBSUB_ITEMS: '_onItems', - PUBSUB_RETRACT: '_onRetract', - PUBSUB_SUBSCRIPTIONS: '_onSubscriptions', - PUBSUB_UNSUBSCRIBE: '_onUnsubscribe', - - PUBSUB_AFFILIATIONS_GET: '_onAffiliationsGet', - PUBSUB_AFFILIATIONS_SET: '_onAffiliationsSet', - PUBSUB_CONFIGURE_GET: '_onConfigureGet', - PUBSUB_CONFIGURE_SET: '_onConfigureSet', - PUBSUB_DEFAULT: '_onDefault', - PUBSUB_PURGE: '_onPurge', - PUBSUB_DELETE: '_onDelete', - PUBSUB_SUBSCRIPTIONS_GET: '_onSubscriptionsGet', - PUBSUB_SUBSCRIPTIONS_SET: '_onSubscriptionsSet', - + '/*': '_onPubSubRequest', } @@ -530,10 +810,7 @@ def connectionMade(self): - self.xmlstream.addObserver(PUBSUB_GET, self.handleRequest) - self.xmlstream.addObserver(PUBSUB_SET, self.handleRequest) - self.xmlstream.addObserver(PUBSUB_OWNER_GET, self.handleRequest) - self.xmlstream.addObserver(PUBSUB_OWNER_SET, self.handleRequest) + self.xmlstream.addObserver(PUBSUB_REQUEST, self.handleRequest) def getDiscoInfo(self, requestor, target, nodeIdentifier): @@ -585,92 +862,17 @@ return d - def _findForm(self, element, formNamespace): - if not element: - return None + def _onPubSubRequest(self, iq): + request = PubSubRequest.fromElement(iq) + handler = getattr(self, '_on_%s' % request.verb) + return handler(request) - form = None - for child in element.elements(): - try: - form = data_form.Form.fromElement(child) - except data_form.Error: - continue - if form.formNamespace != NS_PUBSUB_NODE_CONFIG: - continue + def _on_publish(self, request): + return self.publish(request) - return form - - def _getParameter_node(self, commandElement): - try: - return commandElement["node"] - except KeyError: - raise BadRequest('nodeid-required') - - - def _getParameter_nodeOrEmpty(self, commandElement): - return commandElement.getAttribute("node", '') - - - def _getParameter_jid(self, commandElement): - try: - return jid.internJID(commandElement["jid"]) - except KeyError: - raise BadRequest('jid-required') - - - def _getParameter_max_items(self, commandElement): - value = commandElement.getAttribute('max_items') - - if value: - try: - return int(value) - except ValueError: - raise BadRequest(text="Field max_items requires a positive " + - "integer value") - else: - return None - - - def _getParameters(self, iq, *names): - requestor = jid.internJID(iq["from"]).userhostJID() - service = jid.internJID(iq["to"]) - - params = [requestor, service] - - if names: - command = names[0] - commandElement = getattr(iq.pubsub, command) - if not commandElement: - raise Exception("Could not find command element %r" % command) - - for name in names[1:]: - try: - getter = getattr(self, '_getParameter_' + name) - except KeyError: - raise Exception("No parameter getter for this name") - - params.append(getter(commandElement)) - - return params - - - def _onPublish(self, iq): - requestor, service, nodeIdentifier = self._getParameters( - iq, 'publish', 'node') - - items = [] - for element in iq.pubsub.publish.elements(): - if element.uri == NS_PUBSUB and element.name == 'item': - items.append(element) - - return self.publish(requestor, service, nodeIdentifier, items) - - - def _onSubscribe(self, iq): - requestor, service, nodeIdentifier, subscriber = self._getParameters( - iq, 'subscribe', 'nodeOrEmpty', 'jid') + def _on_subscribe(self, request): def toResponse(result): response = domish.Element((NS_PUBSUB, "pubsub")) @@ -681,28 +883,24 @@ subscription["subscription"] = result.state return response - d = self.subscribe(requestor, service, nodeIdentifier, subscriber) + d = self.subscribe(request) d.addCallback(toResponse) return d - def _onUnsubscribe(self, iq): - requestor, service, nodeIdentifier, subscriber = self._getParameters( - iq, 'unsubscribe', 'nodeOrEmpty', 'jid') + def _on_unsubscribe(self, request): + return self.unsubscribe(request) - return self.unsubscribe(requestor, service, nodeIdentifier, subscriber) - - def _onOptionsGet(self, iq): + def _on_optionsGet(self, request): raise Unsupported('subscription-options') - def _onOptionsSet(self, iq): + def _on_optionsSet(self, request): raise Unsupported('subscription-options') - def _onSubscriptions(self, iq): - requestor, service = self._getParameters(iq) + def _on_subscriptions(self, request): def toResponse(result): response = domish.Element((NS_PUBSUB, 'pubsub')) @@ -714,13 +912,12 @@ item['subscription'] = subscription.state return response - d = self.subscriptions(requestor, service) + d = self.subscriptions(request) d.addCallback(toResponse) return d - def _onAffiliations(self, iq): - requestor, service = self._getParameters(iq) + def _on_affiliations(self, request): def toResponse(result): response = domish.Element((NS_PUBSUB, 'pubsub')) @@ -733,17 +930,15 @@ return response - d = self.affiliations(requestor, service) + d = self.affiliations(request) d.addCallback(toResponse) return d - def _onCreate(self, iq): - requestor, service = self._getParameters(iq) - nodeIdentifier = iq.pubsub.create.getAttribute("node") + def _on_create(self, request): def toResponse(result): - if not nodeIdentifier or nodeIdentifier != result: + if not request.nodeIdentifier or request.nodeIdentifier != result: response = domish.Element((NS_PUBSUB, 'pubsub')) create = response.addElement('create') create['node'] = result @@ -751,7 +946,7 @@ else: return None - d = self.create(requestor, service, nodeIdentifier) + d = self.create(request) d.addCallback(toResponse) return d @@ -771,6 +966,7 @@ fields.append(data_form.Field.fromDict(option)) return fields + def _formFromConfiguration(self, values): options = self.getConfigurationOptions() fields = self._makeFields(options, values) @@ -780,6 +976,7 @@ return form + def _checkConfiguration(self, values): options = self.getConfigurationOptions() processedValues = {} @@ -805,8 +1002,7 @@ return processedValues - def _onDefault(self, iq): - requestor, service = self._getParameters(iq) + def _on_default(self, request): def toResponse(options): response = domish.Element((NS_PUBSUB_OWNER, "pubsub")) @@ -814,127 +1010,82 @@ default.addChild(self._formFromConfiguration(options).toElement()) return response - form = self._findForm(iq.pubsub.config, NS_PUBSUB_NODE_CONFIG) - values = form and form.formType == 'result' and form.getValues() or {} - nodeType = values.get('pubsub#node_type', 'leaf') - - if nodeType not in ('leaf', 'collections'): + if request.nodeType not in ('leaf', 'collection'): return defer.fail(error.StanzaError('not-acceptable')) - d = self.getDefaultConfiguration(requestor, service, nodeType) + d = self.getDefaultConfiguration(request) d.addCallback(toResponse) return d - def _onConfigureGet(self, iq): - requestor, service, nodeIdentifier = self._getParameters( - iq, 'configure', 'nodeOrEmpty') - + def _on_configureGet(self, request): def toResponse(options): response = domish.Element((NS_PUBSUB_OWNER, "pubsub")) configure = response.addElement("configure") - configure.addChild(self._formFromConfiguration(options).toElement()) + form = self._formFromConfiguration(options) + configure.addChild(form.toElement()) - if nodeIdentifier: - configure["node"] = nodeIdentifier + if request.nodeIdentifier: + configure["node"] = request.nodeIdentifier return response - d = self.getConfiguration(requestor, service, nodeIdentifier) + d = self.getConfiguration(request) d.addCallback(toResponse) return d - def _onConfigureSet(self, iq): - requestor, service, nodeIdentifier = self._getParameters( - iq, 'configure', 'nodeOrEmpty') + def _on_configureSet(self, request): + if request.options: + request.options = self._checkConfiguration(request.options) + return self.setConfiguration(request) + else: + return None - # Search configuration form with correct FORM_TYPE and process it - form = self._findForm(iq.pubsub.configure, NS_PUBSUB_NODE_CONFIG) - if form: - if form.formType == 'submit': - options = self._checkConfiguration(form.getValues()) - - return self.setConfiguration(requestor, service, - nodeIdentifier, options) - elif form.formType == 'cancel': - return None - - raise BadRequest() - - - def _onItems(self, iq): - requestor, service, nodeIdentifier, maxItems = self._getParameters( - iq, 'items', 'nodeOrEmpty', 'max_items') - - itemIdentifiers = [] - for child in iq.pubsub.items.elements(): - if child.name == 'item' and child.uri == NS_PUBSUB: - try: - itemIdentifiers.append(child["id"]) - except KeyError: - raise BadRequest() + def _on_items(self, request): def toResponse(result): response = domish.Element((NS_PUBSUB, 'pubsub')) items = response.addElement('items') - if nodeIdentifier: - items["node"] = nodeIdentifier + items["node"] = request.nodeIdentifier for item in result: items.addChild(item) return response - d = self.items(requestor, service, nodeIdentifier, maxItems, - itemIdentifiers) + d = self.items(request) d.addCallback(toResponse) return d - def _onRetract(self, iq): - requestor, service, nodeIdentifier = self._getParameters( - iq, 'retract', 'node') + def _on_retract(self, request): + return self.retract(request) - itemIdentifiers = [] - for child in iq.pubsub.retract.elements(): - if child.uri == NS_PUBSUB and child.name == 'item': - try: - itemIdentifiers.append(child["id"]) - except KeyError: - raise BadRequest() - return self.retract(requestor, service, nodeIdentifier, - itemIdentifiers) + def _on_purge(self, request): + return self.purge(request) - def _onPurge(self, iq): - requestor, service, nodeIdentifier = self._getParameters( - iq, 'purge', 'node') - return self.purge(requestor, service, nodeIdentifier) + def _on_delete(self, request): + return self.delete(request) - def _onDelete(self, iq): - requestor, service, nodeIdentifier = self._getParameters( - iq, 'delete', 'node') - return self.delete(requestor, service, nodeIdentifier) - - - def _onAffiliationsGet(self, iq): + def _on_affiliationsGet(self, iq): raise Unsupported('modify-affiliations') - def _onAffiliationsSet(self, iq): + def _on_affiliationsSet(self, iq): raise Unsupported('modify-affiliations') - def _onSubscriptionsGet(self, iq): + def _on_subscriptionsGet(self, iq): raise Unsupported('manage-subscriptions') - def _onSubscriptionsSet(self, iq): + def _on_subscriptionsSet(self, iq): raise Unsupported('manage-subscriptions') # public methods @@ -990,27 +1141,27 @@ return [] - def publish(self, requestor, service, nodeIdentifier, items): + def publish(self, request): raise Unsupported('publish') - def subscribe(self, requestor, service, nodeIdentifier, subscriber): + def subscribe(self, request): raise Unsupported('subscribe') - def unsubscribe(self, requestor, service, nodeIdentifier, subscriber): + def unsubscribe(self, request): raise Unsupported('subscribe') - def subscriptions(self, requestor, service): + def subscriptions(self, request): raise Unsupported('retrieve-subscriptions') - def affiliations(self, requestor, service): + def affiliations(self, request): raise Unsupported('retrieve-affiliations') - def create(self, requestor, service, nodeIdentifier): + def create(self, request): raise Unsupported('create-nodes') @@ -1018,30 +1169,29 @@ return {} - def getDefaultConfiguration(self, requestor, service, nodeType): + def getDefaultConfiguration(self, request): raise Unsupported('retrieve-default') - def getConfiguration(self, requestor, service, nodeIdentifier): + def getConfiguration(self, request): raise Unsupported('config-node') - def setConfiguration(self, requestor, service, nodeIdentifier, options): + def setConfiguration(self, request): raise Unsupported('config-node') - def items(self, requestor, service, nodeIdentifier, maxItems, - itemIdentifiers): + def items(self, request): raise Unsupported('retrieve-items') - def retract(self, requestor, service, nodeIdentifier, itemIdentifiers): + def retract(self, request): raise Unsupported('retract-items') - def purge(self, requestor, service, nodeIdentifier): + def purge(self, request): raise Unsupported('purge-nodes') - def delete(self, requestor, service, nodeIdentifier): + def delete(self, request): raise Unsupported('delete-nodes') diff -r 0bfc0b2a633c wokkel/test/test_pubsub.py --- a/wokkel/test/test_pubsub.py Wed Apr 01 17:25:11 2009 +0200 +++ b/wokkel/test/test_pubsub.py Wed Apr 01 17:26:46 2009 +0200 @@ -14,6 +14,7 @@ from twisted.words.protocols.jabber.jid import JID from wokkel import data_form, iwokkel, pubsub, shim +from wokkel.generic import parseXml from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub try: @@ -490,6 +491,630 @@ +class PubSubRequestTest(unittest.TestCase): + + 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_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_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_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) + + + 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) + + + def test_fromElementOptionsSetCancel(self): + """ + Test parsing a request for cancelling setting subscription options. + """ + + xml = """ + + + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual({}, request.options) + + + def test_fromElementOptionsSetBadFormType(self): + """ + On a options set request unknown fields should be ignored. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + 1 + + + + + """ + + err = self.assertRaises(error.StanzaError, + pubsub.PubSubRequest.fromElement, + parseXml(xml)) + self.assertEqual('bad-request', err.condition) + 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) + + + 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_fromElementDefault(self): + """ + Test parsing a request for the default node configuration. + """ + + xml = """ + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('default', request.verb) + self.assertEqual(JID('user@example.org'), request.sender) + self.assertEqual(JID('pubsub.example.org'), request.recipient) + self.assertEqual('leaf', request.nodeType) + + + def test_fromElementDefaultCollection(self): + """ + Parsing a request for the default configuration extracts the node type. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + + collection + + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual('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) + + + def test_fromElementConfigureSetCancel(self): + """ + The node configuration is cancelled, so no options. + """ + + xml = """ + + + + + + + + """ + + request = pubsub.PubSubRequest.fromElement(parseXml(xml)) + self.assertEqual({}, request.options) + + + def test_fromElementConfigureSetBadFormType(self): + """ + On a node configuration set request unknown fields should be ignored. + """ + + 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(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.assertEqual([], request.itemIdentifiers) + + + 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}. @@ -507,6 +1132,29 @@ verify.verifyObject(iwokkel.IPubSubService, 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. @@ -524,28 +1172,6 @@ return d - def test_onPublishNoNode(self): - """ - The root node is always a collection, publishing is a bad request. - """ - 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_onPublish(self): """ A publish request should result in L{PubSubService.publish} being @@ -561,27 +1187,147 @@ """ - def publish(requestor, service, nodeIdentifier, items): - self.assertEqual(JID('user@example.org'), requestor) - self.assertEqual(JID('pubsub.example.org'), service) - self.assertEqual('test', nodeIdentifier) - self.assertEqual([], items) + def publish(request): return defer.succeed(None) self.service.publish = publish + verify.verifyObject(iwokkel.IPubSubService, self.service) return self.handleRequest(xml) + def test_onSubscribe(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.service.subscribe = subscribe + verify.verifyObject(iwokkel.IPubSubService, self.service) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_onSubscribeEmptyNode(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.service.subscribe = subscribe + verify.verifyObject(iwokkel.IPubSubService, self.service) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_onUnsubscribe(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.service.unsubscribe = unsubscribe + verify.verifyObject(iwokkel.IPubSubService, self.service) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + def test_onOptionsGet(self): """ - Subscription options are not supported. + 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_onOptionsSet(self): + """ + Setting subscription options is not supported. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#subscribe_options + + 1 + + """ @@ -627,14 +1373,141 @@ self.assertEqual('subscribed', subscription['subscription']) - def subscriptions(requestor, service): - self.assertEqual(JID('user@example.org'), requestor) - self.assertEqual(JID('pubsub.example.org'), service) + def subscriptions(request): subscription = pubsub.Subscription('test', JID('user@example.org'), 'subscribed') return defer.succeed([subscription]) self.service.subscriptions = subscriptions + verify.verifyObject(iwokkel.IPubSubService, self.service) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_onAffiliations(self): + """ + A subscriptions request should result in + L{PubSubService.affiliations} being called and the result prepared + for the response. + """ + + xml = """ + + + + + + """ + + 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']) + + + def affiliations(request): + affiliation = ('test', 'owner') + return defer.succeed([affiliation]) + + self.service.affiliations = affiliations + verify.verifyObject(iwokkel.IPubSubService, self.service) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_onCreate(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.service.create = create + verify.verifyObject(iwokkel.IPubSubService, self.service) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_onCreateChanged(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.service.create = create + verify.verifyObject(iwokkel.IPubSubService, self.service) + d = self.handleRequest(xml) + d.addCallback(cb) + return d + + + def test_onCreateInstant(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.service.create = create + verify.verifyObject(iwokkel.IPubSubService, self.service) d = self.handleRequest(xml) d.addCallback(cb) return d @@ -665,10 +1538,7 @@ "label": "Deliver payloads with event notifications"} } - def getDefaultConfiguration(requestor, service, nodeType): - self.assertEqual(JID('user@example.org'), requestor) - self.assertEqual(JID('pubsub.example.org'), service) - self.assertEqual('leaf', nodeType) + def getDefaultConfiguration(request): return defer.succeed({}) def cb(element): @@ -686,6 +1556,85 @@ return d + def test_onDefaultCollection(self): + """ + Responses to default requests should depend on passed node type. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + + collection + + + + + + + """ + + def getConfigurationOptions(): + return { + "pubsub#deliver_payloads": + {"type": "boolean", + "label": "Deliver payloads with event notifications"} + } + + def getDefaultConfiguration(request): + return defer.succeed({}) + + self.service.getConfigurationOptions = getConfigurationOptions + self.service.getDefaultConfiguration = getDefaultConfiguration + verify.verifyObject(iwokkel.IPubSubService, self.service) + return self.handleRequest(xml) + + + def test_onDefaultUnknownNodeType(self): + """ + A default request should result in + L{PubSubService.getDefaultConfiguration} being called. + """ + + xml = """ + + + + + + http://jabber.org/protocol/pubsub#node_config + + + unknown + + + + + + + """ + + def getDefaultConfiguration(request): + self.fail("Unexpected call to getConfiguration") + + def cb(result): + self.assertEquals('not-acceptable', result.condition) + + self.service.getDefaultConfiguration = getDefaultConfiguration + verify.verifyObject(iwokkel.IPubSubService, self.service) + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + def test_onConfigureGet(self): """ On a node configuration get request L{PubSubService.getConfiguration} @@ -714,14 +1663,11 @@ "label": "Owner of the node"} } - def getConfiguration(requestor, service, nodeIdentifier): - self.assertEqual(JID('user@example.org'), requestor) - self.assertEqual(JID('pubsub.example.org'), service) - self.assertEqual('test', nodeIdentifier) - + def getConfiguration(request): return defer.succeed({'pubsub#deliver_payloads': '0', 'pubsub#persist_items': '1', - 'pubsub#owner': JID('user@example.org')}) + 'pubsub#owner': JID('user@example.org'), + 'x-myfield': ['a', 'b']}) def cb(element): self.assertEqual('pubsub', element.name) @@ -749,8 +1695,12 @@ field.typeCheck() self.assertEqual(JID('user@example.org'), field.value) + self.assertNotIn('x-myfield', fields) + + self.service.getConfigurationOptions = getConfigurationOptions self.service.getConfiguration = getConfiguration + verify.verifyObject(iwokkel.IPubSubService, self.service) d = self.handleRequest(xml) d.addCallback(cb) return d @@ -789,16 +1739,14 @@ "label": "Deliver payloads with event notifications"} } - def setConfiguration(requestor, service, nodeIdentifier, options): - self.assertEqual(JID('user@example.org'), requestor) - self.assertEqual(JID('pubsub.example.org'), service) - self.assertEqual('test', nodeIdentifier) + def setConfiguration(request): self.assertEqual({'pubsub#deliver_payloads': False, - 'pubsub#persist_items': True}, options) + 'pubsub#persist_items': True}, request.options) return defer.succeed(None) self.service.getConfigurationOptions = getConfigurationOptions self.service.setConfiguration = setConfiguration + verify.verifyObject(iwokkel.IPubSubService, self.service) return self.handleRequest(xml) @@ -823,10 +1771,11 @@ """ - def setConfiguration(requestor, service, nodeIdentifier, options): + def setConfiguration(request): self.fail("Unexpected call to setConfiguration") self.service.setConfiguration = setConfiguration + verify.verifyObject(iwokkel.IPubSubService, self.service) return self.handleRequest(xml) @@ -862,14 +1811,47 @@ "label": "Deliver payloads with event notifications"} } - def setConfiguration(requestor, service, nodeIdentifier, options): - self.assertEquals(['pubsub#deliver_payloads'], options.keys()) + def setConfiguration(request): + self.assertEquals(['pubsub#deliver_payloads'], + request.options.keys()) self.service.getConfigurationOptions = getConfigurationOptions self.service.setConfiguration = setConfiguration + verify.verifyObject(iwokkel.IPubSubService, self.service) return self.handleRequest(xml) + def test_onConfigureSetBadFormType(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) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + def test_onItems(self): """ On a items request, return all items for the given node. @@ -883,12 +1865,7 @@ """ - def items(requestor, service, nodeIdentifier, maxItems, items): - self.assertEqual(JID('user@example.org'), requestor) - self.assertEqual(JID('pubsub.example.org'), service) - self.assertEqual('test', nodeIdentifier) - self.assertIdentical(None, maxItems) - self.assertEqual([], items) + def items(request): return defer.succeed([pubsub.Item('current')]) def cb(element): @@ -925,11 +1902,7 @@ """ - def retract(requestor, service, nodeIdentifier, itemIdentifiers): - self.assertEqual(JID('user@example.org'), requestor) - self.assertEqual(JID('pubsub.example.org'), service) - self.assertEqual('test', nodeIdentifier) - self.assertEqual(['item1', 'item2'], itemIdentifiers) + def retract(request): return defer.succeed(None) self.service.retract = retract @@ -951,10 +1924,7 @@ """ - def purge(requestor, service, nodeIdentifier): - self.assertEqual(JID('user@example.org'), requestor) - self.assertEqual(JID('pubsub.example.org'), service) - self.assertEqual('test', nodeIdentifier) + def purge(request): return defer.succeed(None) self.service.purge = purge @@ -976,10 +1946,7 @@ """ - def delete(requestor, service, nodeIdentifier): - self.assertEqual(JID('user@example.org'), requestor) - self.assertEqual(JID('pubsub.example.org'), service) - self.assertEqual('test', nodeIdentifier) + def delete(request): return defer.succeed(None) self.service.delete = delete @@ -1031,3 +1998,461 @@ 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_onSubscriptionsGet(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_onSubscriptionsSet(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_onAffiliationsGet(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('modify-affiliations', + result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + d.addCallback(cb) + return d + + + def test_onAffiliationsSet(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('modify-affiliations', + result.appCondition['feature']) + + d = self.handleRequest(xml) + self.assertFailure(d, error.StanzaError) + 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_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