source: ralphm-patches/pubsub_request.patch @ 12:fc40892815eb

Last change on this file since 12:fc40892815eb was 7:0fbac5c2e97d, checked in by Ralph Meijer <ralphm@…>, 13 years ago

Save current state.

File size: 106.9 KB
RevLine 
[1]1Add PubSubRequest that parses requests and replaces separate parameters.
2
3The publish-subscribe protocol has a large number of optional features. Adding
4support for a feature often means more information needs to be passed, and
5required changing the signature of the methods that need to be overridden by
6adaptors.
7
8This change adds the new PubSubRequest class that features a fromElement
9method that creates a new instance, parses a request and then fills the
10instance attributes where appropriate. This instance is then the only argument
11that is passed to the corresponding handler methods.
12
[7]13diff -r 556fc011f965 wokkel/generic.py
14--- a/wokkel/generic.py Tue Apr 07 11:16:01 2009 +0200
15+++ b/wokkel/generic.py Tue Apr 07 11:17:21 2009 +0200
[4]16@@ -10,7 +10,7 @@
17 from zope.interface import implements
18 
[6]19 from twisted.internet import defer, protocol
20-from twisted.words.protocols.jabber import error, xmlstream
21+from twisted.words.protocols.jabber import error, jid, xmlstream
[4]22 from twisted.words.protocols.jabber.xmlstream import toResponse
23 from twisted.words.xish import domish, utility
24 
[6]25@@ -162,6 +162,37 @@
[4]26         self.sink.send = lambda obj: self.source.dispatch(obj)
[6]27 
28 
[4]29+
30+class Stanza(object):
31+    """
32+    Abstract representation of a stanza.
33+
34+    @ivar sender: The sending entity.
35+    @type sender: L{jid.JID}
36+    @ivar recipient: The receiving entity.
37+    @type recipient: L{jid.JID}
38+    """
39+
40+    sender = None
41+    recipient = None
42+    stanzaType = None
43+
44+    @classmethod
45+    def fromElement(Class, element):
46+        stanza = Class()
47+        stanza.parseElement(element)
48+        return stanza
49+
50+
51+    def parseElement(self, element):
52+        self.sender = jid.internJID(element['from'])
53+        if element.hasAttribute('from'):
54+            self.sender = jid.internJID(element['from'])
55+        if element.hasAttribute('to'):
56+            self.recipient = jid.internJID(element['to'])
57+        self.stanzaType = element.getAttribute('type')
[6]58+
59+
60 class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory):
61     protocol = xmlstream.XmlStream
62 
[7]63diff -r 556fc011f965 wokkel/iwokkel.py
64--- a/wokkel/iwokkel.py Tue Apr 07 11:16:01 2009 +0200
65+++ b/wokkel/iwokkel.py Tue Apr 07 11:17:21 2009 +0200
[1]66@@ -278,6 +278,7 @@
67                              C{list} of L{domish.Element})
68         """
69 
70+
71     def notifyDelete(service, nodeIdentifier, subscribers,
72                      redirectURI=None):
73         """
74@@ -295,77 +296,60 @@
75         @type redirectURI: C{str}
76         """
77 
78-    def publish(requestor, service, nodeIdentifier, items):
79+
80+    def publish(request):
81         """
82         Called when a publish request has been received.
83 
84-        @param requestor: The entity the request originated from.
85-        @type requestor: L{jid.JID}
86-        @param service: The entity the request was addressed to.
87-        @type service: L{jid.JID}
88-        @param nodeIdentifier: The identifier of the node to publish to.
89-        @type nodeIdentifier: C{unicode}
90-        @param items: The items to be published as L{domish} elements.
91-        @type items: C{list} of C{domish.Element}
92+        @param request: The publish-subscribe request.
93+        @type request: L{wokkel.pubsub.PubSubRequest}
94         @return: deferred that fires on success.
95         @rtype: L{defer.Deferred}
96         """
97 
98-    def subscribe(requestor, service, nodeIdentifier, subscriber):
99+
100+    def subscribe(request):
101         """
102         Called when a subscribe request has been received.
103 
104-        @param requestor: The entity the request originated from.
105-        @type requestor: L{jid.JID}
106-        @param service: The entity the request was addressed to.
107-        @type service: L{jid.JID}
108-        @param nodeIdentifier: The identifier of the node to subscribe to.
109-        @type nodeIdentifier: C{unicode}
110-        @param subscriber: The entity to be subscribed.
111-        @type subscriber: L{jid.JID}
112+        @param request: The publish-subscribe request.
113+        @type request: L{wokkel.pubsub.PubSubRequest}
114         @return: A deferred that fires with a
115                  L{Subscription<wokkel.pubsub.Subscription>}.
116         @rtype: L{defer.Deferred}
117         """
118 
119-    def unsubscribe(requestor, service, nodeIdentifier, subscriber):
120+
121+    def unsubscribe(request):
122         """
123         Called when a subscribe request has been received.
124 
125-        @param requestor: The entity the request originated from.
126-        @type requestor: L{jid.JID}
127-        @param service: The entity the request was addressed to.
128-        @type service: L{jid.JID}
129-        @param nodeIdentifier: The identifier of the node to unsubscribe from.
130-        @type nodeIdentifier: C{unicode}
131-        @param subscriber: The entity to be unsubscribed.
132-        @type subscriber: L{jid.JID}
133+        @param request: The publish-subscribe request.
134+        @type request: L{wokkel.pubsub.PubSubRequest}
135         @return: A deferred that fires with C{None} when unsubscription has
136                  succeeded.
137         @rtype: L{defer.Deferred}
138         """
139 
140-    def subscriptions(requestor, service):
141+
142+    def subscriptions(request):
143         """
144         Called when a subscriptions retrieval request has been received.
145 
146-        @param requestor: The entity the request originated from.
147-        @type requestor: L{jid.JID}
148-        @param service: The entity the request was addressed to.
149-        @type service: L{jid.JID}
150+        @param request: The publish-subscribe request.
151+        @type request: L{wokkel.pubsub.PubSubRequest}
152         @return: A deferred that fires with a C{list} of subscriptions as
153                  L{Subscription<wokkel.pubsub.Subscription>}.
154         @rtype: L{defer.Deferred}
155         """
156 
157-    def affiliations(requestor, service):
158+
159+    def affiliations(request):
160         """
161         Called when a affiliations retrieval request has been received.
162 
163-        @param requestor: The entity the request originated from.
164-        @type requestor: L{jid.JID}
165-        @param service: The entity the request was addressed to.
166-        @type service: L{jid.JID}
167+        @param request: The publish-subscribe request.
168+        @type request: L{wokkel.pubsub.PubSubRequest}
169         @return: A deferred that fires with a C{list} of affiliations as
170                  C{tuple}s of (node identifier as C{unicode}, affiliation state
171                  as C{str}). The affiliation can be C{'owner'}, C{'publisher'},
[4]172@@ -373,24 +357,19 @@
[1]173         @rtype: L{defer.Deferred}
174         """
175 
176-    def create(requestor, service, nodeIdentifier):
177+
178+    def create(request):
179         """
180         Called when a node creation request has been received.
181 
182-        @param requestor: The entity the request originated from.
183-        @type requestor: L{jid.JID}
184-        @param service: The entity the request was addressed to.
185-        @type service: L{jid.JID}
186-        @param nodeIdentifier: The suggestion for the identifier of the node to
187-                               be created. If the request did not include a
188-                               suggestion for the node identifier, the value
189-                               is C{None}.
190-        @type nodeIdentifier: C{unicode} or C{NoneType}
191+        @param request: The publish-subscribe request.
192+        @type request: L{wokkel.pubsub.PubSubRequest}
193         @return: A deferred that fires with a C{unicode} that represents
194                  the identifier of the new node.
195         @rtype: L{defer.Deferred}
196         """
[4]197 
[1]198+
199     def getConfigurationOptions():
200         """
[4]201         Retrieve all known node configuration options.
[1]202@@ -426,17 +405,13 @@
203         @rtype: C{dict}.
204         """
205 
206-    def getDefaultConfiguration(requestor, service, nodeType):
207+
208+    def getDefaultConfiguration(request):
209         """
210         Called when a default node configuration request has been received.
211 
212-        @param requestor: The entity the request originated from.
213-        @type requestor: L{jid.JID}
214-        @param service: The entity the request was addressed to.
215-        @type service: L{jid.JID}
216-        @param nodeType: The type of node for which the configuration is
217-                         retrieved, C{'leaf'} or C{'collection'}.
218-        @type nodeType: C{str}
219+        @param request: The publish-subscribe request.
220+        @type request: L{wokkel.pubsub.PubSubRequest}
221         @return: A deferred that fires with a C{dict} representing the default
222                  node configuration. Keys are C{str}s that represent the
223                  field name. Values can be of types C{unicode}, C{int} or
224@@ -444,85 +419,74 @@
225         @rtype: L{defer.Deferred}
226         """
227 
228-    def getConfiguration(requestor, service, nodeIdentifier):
229+
230+    def getConfiguration(request):
231         """
232         Called when a node configuration retrieval request has been received.
233 
234-        @param requestor: The entity the request originated from.
235-        @type requestor: L{jid.JID}
236-        @param service: The entity the request was addressed to.
237-        @type service: L{jid.JID}
238-        @param nodeIdentifier: The identifier of the node to retrieve the
239-                               configuration from.
240-        @type nodeIdentifier: C{unicode}
241+        @param request: The publish-subscribe request.
242+        @type request: L{wokkel.pubsub.PubSubRequest}
243         @return: A deferred that fires with a C{dict} representing the node
244                  configuration. Keys are C{str}s that represent the field name.
245                  Values can be of types C{unicode}, C{int} or C{bool}.
246         @rtype: L{defer.Deferred}
247         """
248 
249-    def setConfiguration(requestor, service, nodeIdentifier, options):
250+
251+    def setConfiguration(request):
252         """
253         Called when a node configuration change request has been received.
254 
255-        @param requestor: The entity the request originated from.
256-        @type requestor: L{jid.JID}
257-        @param service: The entity the request was addressed to.
258-        @type service: L{jid.JID}
259-        @param nodeIdentifier: The identifier of the node to change the
260-                               configuration of.
261-        @type nodeIdentifier: C{unicode}
262+        @param request: The publish-subscribe request.
263+        @type request: L{wokkel.pubsub.PubSubRequest}
264         @return: A deferred that fires with C{None} when the node's
265                  configuration has been changed.
266         @rtype: L{defer.Deferred}
267         """
268 
269-    def items(requestor, service, nodeIdentifier, maxItems, itemIdentifiers):
270+
271+    def items(request):
272         """
273         Called when a items retrieval request has been received.
274 
275-        @param requestor: The entity the request originated from.
276-        @type requestor: L{jid.JID}
277-        @param service: The entity the request was addressed to.
278-        @type service: L{jid.JID}
279-        @param nodeIdentifier: The identifier of the node to retrieve items
280-                               from.
281-        @type nodeIdentifier: C{unicode}
282+        @param request: The publish-subscribe request.
283+        @type request: L{wokkel.pubsub.PubSubRequest}
284+        @return: A deferred that fires with a C{list} of L{pubsub.Item}.
285+        @rtype: L{defer.Deferred}
286         """
287 
288-    def retract(requestor, service, nodeIdentifier, itemIdentifiers):
289+
290+    def retract(request):
291         """
292         Called when a item retraction request has been received.
293 
294-        @param requestor: The entity the request originated from.
295-        @type requestor: L{jid.JID}
296-        @param service: The entity the request was addressed to.
297-        @type service: L{jid.JID}
298-        @param nodeIdentifier: The identifier of the node to retract items
299-                               from.
300-        @type nodeIdentifier: C{unicode}
301+        @param request: The publish-subscribe request.
302+        @type request: L{wokkel.pubsub.PubSubRequest}
303+        @return: A deferred that fires with C{None} when the given items have
304+                 been retracted.
305+        @rtype: L{defer.Deferred}
306         """
307 
308-    def purge(requestor, service, nodeIdentifier):
309+
310+    def purge(request):
311         """
312         Called when a node purge request has been received.
313 
314-        @param requestor: The entity the request originated from.
315-        @type requestor: L{jid.JID}
316-        @param service: The entity the request was addressed to.
317-        @type service: L{jid.JID}
318-        @param nodeIdentifier: The identifier of the node to be purged.
319-        @type nodeIdentifier: C{unicode}
320+        @param request: The publish-subscribe request.
321+        @type request: L{wokkel.pubsub.PubSubRequest}
322+        @return: A deferred that fires with C{None} when the node has been
323+                 purged.
324+        @rtype: L{defer.Deferred}
325         """
326 
327-    def delete(requestor, service, nodeIdentifier):
328+
329+    def delete(request):
330         """
331         Called when a node deletion request has been received.
332 
333-        @param requestor: The entity the request originated from.
334-        @type requestor: L{jid.JID}
335-        @param service: The entity the request was addressed to.
336-        @type service: L{jid.JID}
337-        @param nodeIdentifier: The identifier of the node to be delete.
338-        @type nodeIdentifier: C{unicode}
339+        @param request: The publish-subscribe request.
340+        @type request: L{wokkel.pubsub.PubSubRequest}
341+        @return: A deferred that fires with C{None} when the node has been
342+                 deleted.
343+        @rtype: L{defer.Deferred}
344         """
[7]345diff -r 556fc011f965 wokkel/pubsub.py
346--- a/wokkel/pubsub.py  Tue Apr 07 11:16:01 2009 +0200
347+++ b/wokkel/pubsub.py  Tue Apr 07 11:17:21 2009 +0200
[4]348@@ -16,7 +16,7 @@
349 from twisted.words.protocols.jabber import jid, error, xmlstream
350 from twisted.words.xish import domish
351 
352-from wokkel import disco, data_form, shim
353+from wokkel import disco, data_form, generic, shim
354 from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
355 from wokkel.iwokkel import IPubSubClient, IPubSubService
356 
[5]357@@ -31,43 +31,12 @@
358 NS_PUBSUB_OWNER = NS_PUBSUB + "#owner"
[1]359 NS_PUBSUB_NODE_CONFIG = NS_PUBSUB + "#node_config"
360 NS_PUBSUB_META_DATA = NS_PUBSUB + "#meta-data"
[5]361+NS_PUBSUB_SUBSCRIBE_OPTIONS = NS_PUBSUB + "#subscribe_options"
[1]362 
363-# In publish-subscribe namespace XPath query selector.
364-IN_NS_PUBSUB = '[@xmlns="' + NS_PUBSUB + '"]'
365-IN_NS_PUBSUB_OWNER = '[@xmlns="' + NS_PUBSUB_OWNER + '"]'
366-
367-# Publish-subscribe XPath queries
368-PUBSUB_ELEMENT = '/pubsub' + IN_NS_PUBSUB
369-PUBSUB_OWNER_ELEMENT = '/pubsub' + IN_NS_PUBSUB_OWNER
370-PUBSUB_GET = IQ_GET + PUBSUB_ELEMENT
371-PUBSUB_SET = IQ_SET + PUBSUB_ELEMENT
372-PUBSUB_OWNER_GET = IQ_GET + PUBSUB_OWNER_ELEMENT
373-PUBSUB_OWNER_SET = IQ_SET + PUBSUB_OWNER_ELEMENT
374-
375-# Publish-subscribe command XPath queries
376-PUBSUB_PUBLISH = PUBSUB_SET + '/publish' + IN_NS_PUBSUB
377-PUBSUB_CREATE = PUBSUB_SET + '/create' + IN_NS_PUBSUB
378-PUBSUB_SUBSCRIBE = PUBSUB_SET + '/subscribe' + IN_NS_PUBSUB
379-PUBSUB_UNSUBSCRIBE = PUBSUB_SET + '/unsubscribe' + IN_NS_PUBSUB
380-PUBSUB_OPTIONS_GET = PUBSUB_GET + '/options' + IN_NS_PUBSUB
381-PUBSUB_OPTIONS_SET = PUBSUB_SET + '/options' + IN_NS_PUBSUB
382-PUBSUB_DEFAULT = PUBSUB_OWNER_GET + '/default' + IN_NS_PUBSUB_OWNER
383-PUBSUB_CONFIGURE_GET = PUBSUB_OWNER_GET + '/configure' + IN_NS_PUBSUB_OWNER
384-PUBSUB_CONFIGURE_SET = PUBSUB_OWNER_SET + '/configure' + IN_NS_PUBSUB_OWNER
385-PUBSUB_SUBSCRIPTIONS = PUBSUB_GET + '/subscriptions' + IN_NS_PUBSUB
386-PUBSUB_AFFILIATIONS = PUBSUB_GET + '/affiliations' + IN_NS_PUBSUB
387-PUBSUB_AFFILIATIONS_GET = PUBSUB_OWNER_GET + '/affiliations' + \
388-                          IN_NS_PUBSUB_OWNER
389-PUBSUB_AFFILIATIONS_SET = PUBSUB_OWNER_SET + '/affiliations' + \
390-                          IN_NS_PUBSUB_OWNER
391-PUBSUB_SUBSCRIPTIONS_GET = PUBSUB_OWNER_GET + '/subscriptions' + \
392-                          IN_NS_PUBSUB_OWNER
393-PUBSUB_SUBSCRIPTIONS_SET = PUBSUB_OWNER_SET + '/subscriptions' + \
394-                          IN_NS_PUBSUB_OWNER
395-PUBSUB_ITEMS = PUBSUB_GET + '/items' + IN_NS_PUBSUB
396-PUBSUB_RETRACT = PUBSUB_SET + '/retract' + IN_NS_PUBSUB
397-PUBSUB_PURGE = PUBSUB_OWNER_SET + '/purge' + IN_NS_PUBSUB_OWNER
398-PUBSUB_DELETE = PUBSUB_OWNER_SET + '/delete' + IN_NS_PUBSUB_OWNER
399+# XPath to match pubsub requests
400+PUBSUB_REQUEST = '/iq[@type="get" or @type="set"]/' + \
401+                    'pubsub[@xmlns="' + NS_PUBSUB + '" or ' + \
402+                           '@xmlns="' + NS_PUBSUB_OWNER + '"]'
403 
404 class SubscriptionPending(Exception):
405     """
[5]406@@ -98,12 +67,18 @@
407 
408 
409 
410-class BadRequest(PubSubError):
411+class BadRequest(error.StanzaError):
412     """
413     Bad request stanza error.
414     """
415     def __init__(self, pubsubCondition=None, text=None):
416-        PubSubError.__init__(self, 'bad-request', pubsubCondition, text)
417+        if pubsubCondition:
418+            appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition))
419+        else:
420+            appCondition = None
421+        error.StanzaError.__init__(self, 'bad-request',
422+                                         text=text,
423+                                         appCondition=appCondition)
424 
425 
426 
427@@ -167,40 +142,362 @@
[1]428 
429 
430 
[4]431-class _PubSubRequest(xmlstream.IQ):
432+class PubSubRequest(generic.Stanza):
433     """
434-    Publish subscribe request.
[1]435+    A publish-subscribe request.
[4]436 
437-    @ivar verb: Request verb
438-    @type verb: C{str}
439-    @ivar namespace: Request namespace.
440-    @type namespace: C{str}
441-    @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'}
442-    @type method: C{str}
443-    @ivar command: Command element of the request. This is the direct child of
444-                   the C{pubsub} element in the C{namespace} with the name
445-                   C{verb}.
[1]446+    The set of instance variables used depends on the type of request. If
447+    a variable is not applicable or not passed in the request, its value is
448+    C{None}.
449+
[4]450+    @ivar verb: The type of publish-subscribe request. See L{_requestVerbMap}.
451+    @type verb: C{str}.
452+
[1]453+    @ivar affiliations: Affiliations to be modified.
454+    @type affiliations: C{set}
455+    @ivar items: The items to be published, as L{domish.Element}s.
456+    @type items: C{list}
457+    @ivar itemIdentifiers: Identifiers of the items to be retrieved or
458+                           retracted.
459+    @type itemIdentifiers: C{set}
460+    @ivar maxItems: Maximum number of items to retrieve.
461+    @type maxItems: C{int}.
462+    @ivar nodeIdentifier: Identifier of the node the request is about.
463+    @type nodeIdentifier: C{unicode}
464+    @ivar nodeType: The type of node that should be created, or for which the
465+                    configuration is retrieved. C{'leaf'} or C{'collection'}.
466+    @type nodeType: C{str}
467+    @ivar options: Configurations options for nodes, subscriptions and publish
468+                   requests.
469+    @type options: L{data_form.Form}
470+    @ivar subscriber: The subscribing entity.
471+    @type subscriber: L{JID}
472+    @ivar subscriptionIdentifier: Identifier for a specific subscription.
473+    @type subscriptionIdentifier: C{unicode}
474+    @ivar subscriptions: Subscriptions to be modified, as a set of
475+                         L{Subscription}.
476+    @type subscriptions: C{set}
[4]477     """
478 
479-    def __init__(self, xs, verb, namespace=NS_PUBSUB, method='set'):
480-        xmlstream.IQ.__init__(self, xs, method)
481-        self.addElement((namespace, 'pubsub'))
[1]482+    verb = None
[4]483 
484-        self.command = self.pubsub.addElement(verb)
[1]485+    affiliations = None
486+    items = None
487+    itemIdentifiers = None
488+    maxItems = None
489+    nodeIdentifier = None
490+    nodeType = None
491+    options = None
492+    subscriber = None
493+    subscriptionIdentifier = None
494+    subscriptions = None
[4]495 
496+    # Map request iq type and subelement name to request verb
497+    _requestVerbMap = {
498+        ('set', NS_PUBSUB, 'publish'): 'publish',
499+        ('set', NS_PUBSUB, 'subscribe'): 'subscribe',
500+        ('set', NS_PUBSUB, 'unsubscribe'): 'unsubscribe',
501+        ('get', NS_PUBSUB, 'options'): 'optionsGet',
502+        ('set', NS_PUBSUB, 'options'): 'optionsSet',
503+        ('get', NS_PUBSUB, 'subscriptions'): 'subscriptions',
504+        ('get', NS_PUBSUB, 'affiliations'): 'affiliations',
505+        ('set', NS_PUBSUB, 'create'): 'create',
506+        ('get', NS_PUBSUB_OWNER, 'default'): 'default',
507+        ('get', NS_PUBSUB_OWNER, 'configure'): 'configureGet',
508+        ('set', NS_PUBSUB_OWNER, 'configure'): 'configureSet',
509+        ('get', NS_PUBSUB, 'items'): 'items',
510+        ('set', NS_PUBSUB, 'retract'): 'retract',
511+        ('set', NS_PUBSUB_OWNER, 'purge'): 'purge',
512+        ('set', NS_PUBSUB_OWNER, 'delete'): 'delete',
513+        ('get', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsGet',
514+        ('set', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsSet',
515+        ('get', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsGet',
516+        ('set', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsSet',
517+    }
518 
519-    def send(self, to):
520+    # Map request verb to request iq type and subelement name
521+    _verbRequestMap = dict(((v, k) for k, v in _requestVerbMap.iteritems()))
[1]522+
[4]523+    # Map request verb to parameter handler names
524+    _parameters = {
525+        'publish': ['node', 'items'],
526+        'subscribe': ['nodeOrEmpty', 'jid'],
527+        'unsubscribe': ['nodeOrEmpty', 'jid'],
[5]528+        'optionsGet': ['nodeOrEmpty', 'jid'],
529+        'optionsSet': ['nodeOrEmpty', 'jid', 'options'],
[4]530+        'subscriptions': [],
531+        'affiliations': [],
532+        'create': ['nodeOrNone'],
533+        'default': ['default'],
534+        'configureGet': ['nodeOrEmpty'],
535+        'configureSet': ['nodeOrEmpty', 'configure'],
536+        'items': ['node', 'maxItems', 'itemIdentifiers'],
537+        'retract': ['node', 'itemIdentifiers'],
538+        'purge': ['node'],
539+        'delete': ['node'],
540+        'affiliationsGet': [],
541+        'affiliationsSet': [],
542+        'subscriptionsGet': [],
543+        'subscriptionsSet': [],
544+    }
[1]545+
[4]546+    def __init__(self, verb=None):
547+        self.verb = verb
548+
[1]549+
550+    @staticmethod
551+    def _findForm(element, formNamespace):
[4]552         """
553-        Send out request.
554+        Find a Data Form.
555 
556-        Extends L{xmlstream.IQ.send} by requiring the C{to} parameter to be
557-        a L{JID} instance.
558+        Look for an element that represents a Data Form with the specified
559+        form namespace as a child element of the given element.
560+        """
[1]561+        if not element:
562+            return None
[4]563 
564-        @param to: Entity to send the request to.
565-        @type to: L{JID}
[1]566+        form = None
567+        for child in element.elements():
568+            try:
569+                form = data_form.Form.fromElement(child)
570+            except data_form.Error:
571+                continue
572+
573+            if form.formNamespace != NS_PUBSUB_NODE_CONFIG:
574+                continue
575+
576+        return form
577+
578+
579+    def _parse_node(self, verbElement):
[4]580         """
581-        destination = to.full()
582-        return xmlstream.IQ.send(self, destination)
583+        Parse the required node identifier out of the verbElement.
584+        """
[1]585+        try:
586+            self.nodeIdentifier = verbElement["node"]
587+        except KeyError:
588+            raise BadRequest('nodeid-required')
589+
590+
[4]591+    def _render_node(self, verbElement):
592+        """
593+        Render the required node identifier on the verbElement.
594+        """
595+        if not self.nodeIdentifier:
596+            raise Exception("Node identifier is required")
597+
598+        verbElement['node'] = self.nodeIdentifier
599+
600+
[1]601+    def _parse_nodeOrEmpty(self, verbElement):
[4]602+        """
603+        Parse the node identifier out of the verbElement. May be empty.
604+        """
[1]605+        self.nodeIdentifier = verbElement.getAttribute("node", '')
606+
607+
[4]608+    def _render_nodeOrEmpty(self, verbElement):
609+        """
610+        Render the node identifier on the verbElement. May be empty.
611+        """
612+        if self.nodeIdentifier:
613+            verbElement['node'] = self.nodeIdentifier
614+
615+
[1]616+    def _parse_nodeOrNone(self, verbElement):
[4]617+        """
618+        Parse the optional node identifier out of the verbElement.
619+        """
[1]620+        self.nodeIdentifier = verbElement.getAttribute("node")
621+
622+
[4]623+    def _render_nodeOrNone(self, verbElement):
624+        """
625+        Render the optional node identifier on the verbElement.
626+        """
627+        if self.nodeIdentifier:
628+            verbElement['node'] = self.nodeIdentifier
629+
630+
[1]631+    def _parse_items(self, verbElement):
[4]632+        """
633+        Parse items out of the verbElement for publish requests.
634+        """
[1]635+        self.items = []
636+        for element in verbElement.elements():
637+            if element.uri == NS_PUBSUB and element.name == 'item':
638+                self.items.append(element)
639+
640+
[4]641+    def _render_items(self, verbElement):
642+        """
643+        Render items into the verbElement for publish requests.
644+        """
645+        if self.items:
646+            for item in self.items:
647+                verbElement.addChild(item)
648+
649+
[1]650+    def _parse_jid(self, verbElement):
[4]651+        """
652+        Parse subscriber out of the verbElement for un-/subscribe requests.
653+        """
[1]654+        try:
655+            self.subscriber = jid.internJID(verbElement["jid"])
656+        except KeyError:
657+            raise BadRequest('jid-required')
658+
659+
[4]660+    def _render_jid(self, verbElement):
661+        """
662+        Render subscriber into the verbElement for un-/subscribe requests.
663+        """
664+        verbElement['jid'] = self.subscriber.full()
665+
666+
[1]667+    def _parse_default(self, verbElement):
[4]668+        """
669+        Parse node type out of a request for the default node configuration.
670+        """
[1]671+        form = PubSubRequest._findForm(verbElement, NS_PUBSUB_NODE_CONFIG)
672+        if form and form.formType == 'submit':
673+            values = form.getValues()
674+            self.nodeType = values.get('pubsub#node_type', 'leaf')
675+        else:
676+            self.nodeType = 'leaf'
677+
678+
679+    def _parse_configure(self, verbElement):
[4]680+        """
681+        Parse options out of a request for setting the node configuration.
682+        """
[1]683+        form = PubSubRequest._findForm(verbElement, NS_PUBSUB_NODE_CONFIG)
684+        if form:
685+            if form.formType == 'submit':
686+                self.options = form.getValues()
687+            elif form.formType == 'cancel':
688+                self.options = {}
689+            else:
[5]690+                raise BadRequest(text="Unexpected form type %r" % form.formType)
[1]691+        else:
[5]692+            raise BadRequest(text="Missing configuration form")
[1]693+
694+
695+
696+    def _parse_itemIdentifiers(self, verbElement):
[4]697+        """
698+        Parse item identifiers out of items and retract requests.
699+        """
[1]700+        self.itemIdentifiers = []
701+        for element in verbElement.elements():
702+            if element.uri == NS_PUBSUB and element.name == 'item':
703+                try:
704+                    self.itemIdentifiers.append(element["id"])
705+                except KeyError:
706+                    raise BadRequest()
707+
708+
[4]709+    def _render_itemIdentifiers(self, verbElement):
710+        """
711+        Render item identifiers into items and retract requests.
712+        """
713+        if self.itemIdentifiers:
714+            for itemIdentifier in self.itemIdentifiers:
715+                item = verbElement.addElement('item')
716+                item['id'] = itemIdentifier
717+
718+
[1]719+    def _parse_maxItems(self, verbElement):
[4]720+        """
721+        Parse maximum items out of an items request.
722+        """
[1]723+        value = verbElement.getAttribute('max_items')
724+
725+        if value:
726+            try:
727+                self.maxItems = int(value)
728+            except ValueError:
729+                raise BadRequest(text="Field max_items requires a positive " +
730+                                      "integer value")
731+
732+
[4]733+    def _render_maxItems(self, verbElement):
734+        """
735+        Parse maximum items into an items request.
736+        """
737+        if self.maxItems:
738+            verbElement['max_items'] = unicode(self.maxItems)
739+
740+
[5]741+    def _parse_options(self, verbElement):
742+        form = PubSubRequest._findForm(verbElement, NS_PUBSUB_SUBSCRIBE_OPTIONS)
743+        if form:
744+            if form.formType == 'submit':
745+                self.options = form.getValues()
746+            elif form.formType == 'cancel':
747+                self.options = {}
748+            else:
749+                raise BadRequest(text="Unexpected form type %r" % form.formType)
750+        else:
751+            raise BadRequest(text="Missing options form")
752+
[1]753+    def parseElement(self, element):
[4]754+        """
755+        Parse the publish-subscribe verb and parameters out of a request.
756+        """
757+        generic.Stanza.parseElement(self, element)
[1]758+
759+        for child in element.pubsub.elements():
760+            key = (self.stanzaType, child.uri, child.name)
[4]761+            try:
762+                verb = self._requestVerbMap[key]
763+            except KeyError:
764+                continue
765+            else:
766+                self.verb = verb
[1]767+                break
768+
769+        if not self.verb:
770+            raise NotImplementedError()
771+
[4]772+        for parameter in self._parameters[verb]:
773+            getattr(self, '_parse_%s' % parameter)(child)
[1]774+
775+
[4]776+    def send(self, xs):
777+        """
778+        Send this request to its recipient.
[1]779+
[4]780+        This renders all of the relevant parameters for this specific
781+        requests into an L{xmlstream.IQ}, and invoke its C{send} method.
782+        This returns a deferred that fires upon reception of a response. See
783+        L{xmlstream.IQ} for details.
784+
785+        @param xs: The XML stream to send the request on.
786+        @type xs: L{xmlstream.XmlStream}
787+        @rtype: L{defer.Deferred}.
788+        """
789+
790+        try:
791+            (self.stanzaType,
792+             childURI,
793+             childName) = self._verbRequestMap[self.verb]
794+        except KeyError:
795+            raise NotImplementedError()
796+
797+        iq = xmlstream.IQ(xs, self.stanzaType)
798+        iq.addElement((childURI, 'pubsub'))
799+        verbElement = iq.pubsub.addElement(childName)
800+
801+        if self.sender:
802+            iq['from'] = self.sender.full()
803+        if self.recipient:
804+            iq['to'] = self.recipient.full()
805+
806+        for parameter in self._parameters[self.verb]:
807+            getattr(self, '_render_%s' % parameter)(verbElement)
808+
809+        return iq.send()
810 
811 
812 
[5]813@@ -336,11 +633,9 @@
[4]814         @param nodeIdentifier: Optional suggestion for the id of the node.
815         @type nodeIdentifier: C{unicode}
816         """
817-
818-
819-        request = _PubSubRequest(self.xmlstream, 'create')
820-        if nodeIdentifier:
821-            request.command['node'] = nodeIdentifier
822+        request = PubSubRequest('create')
823+        request.recipient = service
824+        request.nodeIdentifier = nodeIdentifier
825 
826         def cb(iq):
827             try:
[5]828@@ -350,7 +645,9 @@
[4]829                 new_node = nodeIdentifier
830             return new_node
831 
832-        return request.send(service).addCallback(cb)
833+        d = request.send(self.xmlstream)
834+        d.addCallback(cb)
835+        return d
836 
837 
838     def deleteNode(self, service, nodeIdentifier):
[5]839@@ -362,9 +659,10 @@
[4]840         @param nodeIdentifier: The identifier of the node.
841         @type nodeIdentifier: C{unicode}
842         """
843-        request = _PubSubRequest(self.xmlstream, 'delete', NS_PUBSUB_OWNER)
844-        request.command['node'] = nodeIdentifier
845-        return request.send(service)
846+        request = PubSubRequest('delete')
847+        request.recipient = service
848+        request.nodeIdentifier = nodeIdentifier
849+        return request.send(self.xmlstream)
850 
851 
852     def subscribe(self, service, nodeIdentifier, subscriber):
[5]853@@ -379,10 +677,10 @@
[4]854                            will get notifications of new published items.
855         @type subscriber: L{JID}
856         """
857-        request = _PubSubRequest(self.xmlstream, 'subscribe')
858-        if nodeIdentifier:
859-            request.command['node'] = nodeIdentifier
860-        request.command['jid'] = subscriber.full()
861+        request = PubSubRequest('subscribe')
862+        request.recipient = service
863+        request.nodeIdentifier = nodeIdentifier
864+        request.subscriber = subscriber
865 
866         def cb(iq):
867             subscription = iq.pubsub.subscription["subscription"]
[5]868@@ -397,7 +695,9 @@
[4]869                 # yielded a stanza error.
870                 return None
871 
872-        return request.send(service).addCallback(cb)
873+        d = request.send(self.xmlstream)
874+        d.addCallback(cb)
875+        return d
876 
877 
878     def unsubscribe(self, service, nodeIdentifier, subscriber):
[5]879@@ -411,11 +711,11 @@
[4]880         @param subscriber: The entity to unsubscribe from the node.
881         @type subscriber: L{JID}
882         """
883-        request = _PubSubRequest(self.xmlstream, 'unsubscribe')
884-        if nodeIdentifier:
885-            request.command['node'] = nodeIdentifier
886-        request.command['jid'] = subscriber.full()
887-        return request.send(service)
888+        request = PubSubRequest('unsubscribe')
889+        request.recipient = service
890+        request.nodeIdentifier = nodeIdentifier
891+        request.subscriber = subscriber
892+        return request.send(self.xmlstream)
893 
894 
895     def publish(self, service, nodeIdentifier, items=None):
[5]896@@ -429,13 +729,11 @@
[4]897         @param items: Optional list of L{Item}s to publish.
898         @type items: C{list}
899         """
900-        request = _PubSubRequest(self.xmlstream, 'publish')
901-        request.command['node'] = nodeIdentifier
902-        if items:
903-            for item in items:
904-                request.command.addChild(item)
905-
906-        return request.send(service)
907+        request = PubSubRequest('publish')
908+        request.recipient = service
909+        request.nodeIdentifier = nodeIdentifier
910+        request.items = items
911+        return request.send(self.xmlstream)
912 
913 
914     def items(self, service, nodeIdentifier, maxItems=None):
[5]915@@ -449,11 +747,11 @@
[4]916         @param maxItems: Optional limit on the number of retrieved items.
917         @type maxItems: C{int}
918         """
919-        request = _PubSubRequest(self.xmlstream, 'items', method='get')
920-        if nodeIdentifier:
921-            request.command['node'] = nodeIdentifier
922+        request = PubSubRequest('items')
923+        request.recipient = service
924+        request.nodeIdentifier = nodeIdentifier
925         if maxItems:
926-            request.command["max_items"] = str(int(maxItems))
927+            request.maxItems = str(int(maxItems))
928 
929         def cb(iq):
930             items = []
[5]931@@ -462,7 +760,9 @@
[4]932                     items.append(element)
933             return items
934 
935-        return request.send(service).addCallback(cb)
936+        d = request.send(self.xmlstream)
937+        d.addCallback(cb)
938+        return d
939 
940 
941 
[5]942@@ -497,27 +797,7 @@
[1]943     implements(IPubSubService)
944 
945     iqHandlers = {
946-            PUBSUB_PUBLISH: '_onPublish',
947-            PUBSUB_CREATE: '_onCreate',
948-            PUBSUB_SUBSCRIBE: '_onSubscribe',
949-            PUBSUB_OPTIONS_GET: '_onOptionsGet',
950-            PUBSUB_OPTIONS_SET: '_onOptionsSet',
951-            PUBSUB_AFFILIATIONS: '_onAffiliations',
952-            PUBSUB_ITEMS: '_onItems',
953-            PUBSUB_RETRACT: '_onRetract',
954-            PUBSUB_SUBSCRIPTIONS: '_onSubscriptions',
955-            PUBSUB_UNSUBSCRIBE: '_onUnsubscribe',
956-
957-            PUBSUB_AFFILIATIONS_GET: '_onAffiliationsGet',
958-            PUBSUB_AFFILIATIONS_SET: '_onAffiliationsSet',
959-            PUBSUB_CONFIGURE_GET: '_onConfigureGet',
960-            PUBSUB_CONFIGURE_SET: '_onConfigureSet',
961-            PUBSUB_DEFAULT: '_onDefault',
962-            PUBSUB_PURGE: '_onPurge',
963-            PUBSUB_DELETE: '_onDelete',
964-            PUBSUB_SUBSCRIPTIONS_GET: '_onSubscriptionsGet',
965-            PUBSUB_SUBSCRIPTIONS_SET: '_onSubscriptionsSet',
966-
967+            '/*': '_onPubSubRequest',
968             }
969 
970 
[5]971@@ -530,10 +810,7 @@
[1]972 
973 
974     def connectionMade(self):
975-        self.xmlstream.addObserver(PUBSUB_GET, self.handleRequest)
976-        self.xmlstream.addObserver(PUBSUB_SET, self.handleRequest)
977-        self.xmlstream.addObserver(PUBSUB_OWNER_GET, self.handleRequest)
978-        self.xmlstream.addObserver(PUBSUB_OWNER_SET, self.handleRequest)
979+        self.xmlstream.addObserver(PUBSUB_REQUEST, self.handleRequest)
980 
981 
982     def getDiscoInfo(self, requestor, target, nodeIdentifier):
[5]983@@ -585,92 +862,17 @@
[1]984         return d
985 
986 
987-    def _findForm(self, element, formNamespace):
988-        if not element:
989-            return None
990+    def _onPubSubRequest(self, iq):
991+        request = PubSubRequest.fromElement(iq)
[4]992+        handler = getattr(self, '_on_%s' % request.verb)
[1]993+        return handler(request)
994 
995-        form = None
996-        for child in element.elements():
997-            try:
998-                form = data_form.Form.fromElement(child)
999-            except data_form.Error:
1000-                continue
1001 
1002-            if form.formNamespace != NS_PUBSUB_NODE_CONFIG:
1003-                continue
1004+    def _on_publish(self, request):
1005+        return self.publish(request)
1006 
1007-        return form
1008 
1009-
1010-    def _getParameter_node(self, commandElement):
1011-        try:
1012-            return commandElement["node"]
1013-        except KeyError:
1014-            raise BadRequest('nodeid-required')
1015-
1016-
1017-    def _getParameter_nodeOrEmpty(self, commandElement):
1018-        return commandElement.getAttribute("node", '')
1019-
1020-
1021-    def _getParameter_jid(self, commandElement):
1022-        try:
1023-            return jid.internJID(commandElement["jid"])
1024-        except KeyError:
1025-            raise BadRequest('jid-required')
1026-
1027-
1028-    def _getParameter_max_items(self, commandElement):
1029-        value = commandElement.getAttribute('max_items')
1030-
1031-        if value:
1032-            try:
1033-                return int(value)
1034-            except ValueError:
1035-                raise BadRequest(text="Field max_items requires a positive " +
1036-                                      "integer value")
1037-        else:
1038-            return None
1039-
1040-
1041-    def _getParameters(self, iq, *names):
1042-        requestor = jid.internJID(iq["from"]).userhostJID()
1043-        service = jid.internJID(iq["to"])
1044-
1045-        params = [requestor, service]
1046-
1047-        if names:
1048-            command = names[0]
1049-            commandElement = getattr(iq.pubsub, command)
1050-            if not commandElement:
1051-                raise Exception("Could not find command element %r" % command)
1052-
1053-        for name in names[1:]:
1054-            try:
1055-                getter = getattr(self, '_getParameter_' + name)
1056-            except KeyError:
1057-                raise Exception("No parameter getter for this name")
1058-
1059-            params.append(getter(commandElement))
1060-
1061-        return params
1062-
1063-
1064-    def _onPublish(self, iq):
1065-        requestor, service, nodeIdentifier = self._getParameters(
1066-                iq, 'publish', 'node')
1067-
1068-        items = []
1069-        for element in iq.pubsub.publish.elements():
1070-            if element.uri == NS_PUBSUB and element.name == 'item':
1071-                items.append(element)
1072-
1073-        return self.publish(requestor, service, nodeIdentifier, items)
1074-
1075-
1076-    def _onSubscribe(self, iq):
1077-        requestor, service, nodeIdentifier, subscriber = self._getParameters(
1078-                iq, 'subscribe', 'nodeOrEmpty', 'jid')
1079+    def _on_subscribe(self, request):
1080 
1081         def toResponse(result):
1082             response = domish.Element((NS_PUBSUB, "pubsub"))
[5]1083@@ -681,28 +883,24 @@
[1]1084             subscription["subscription"] = result.state
1085             return response
1086 
1087-        d = self.subscribe(requestor, service, nodeIdentifier, subscriber)
1088+        d = self.subscribe(request)
1089         d.addCallback(toResponse)
1090         return d
1091 
1092 
1093-    def _onUnsubscribe(self, iq):
1094-        requestor, service, nodeIdentifier, subscriber = self._getParameters(
1095-                iq, 'unsubscribe', 'nodeOrEmpty', 'jid')
1096+    def _on_unsubscribe(self, request):
1097+        return self.unsubscribe(request)
1098 
1099-        return self.unsubscribe(requestor, service, nodeIdentifier, subscriber)
1100 
1101-
1102-    def _onOptionsGet(self, iq):
1103+    def _on_optionsGet(self, request):
1104         raise Unsupported('subscription-options')
1105 
1106 
1107-    def _onOptionsSet(self, iq):
1108+    def _on_optionsSet(self, request):
1109         raise Unsupported('subscription-options')
1110 
1111 
1112-    def _onSubscriptions(self, iq):
1113-        requestor, service = self._getParameters(iq)
1114+    def _on_subscriptions(self, request):
1115 
1116         def toResponse(result):
1117             response = domish.Element((NS_PUBSUB, 'pubsub'))
[5]1118@@ -714,13 +912,12 @@
[1]1119                 item['subscription'] = subscription.state
1120             return response
1121 
1122-        d = self.subscriptions(requestor, service)
1123+        d = self.subscriptions(request)
1124         d.addCallback(toResponse)
1125         return d
1126 
1127 
1128-    def _onAffiliations(self, iq):
1129-        requestor, service = self._getParameters(iq)
1130+    def _on_affiliations(self, request):
1131 
1132         def toResponse(result):
1133             response = domish.Element((NS_PUBSUB, 'pubsub'))
[5]1134@@ -733,17 +930,15 @@
[1]1135 
1136             return response
1137 
1138-        d = self.affiliations(requestor, service)
1139+        d = self.affiliations(request)
1140         d.addCallback(toResponse)
1141         return d
1142 
1143 
1144-    def _onCreate(self, iq):
1145-        requestor, service = self._getParameters(iq)
1146-        nodeIdentifier = iq.pubsub.create.getAttribute("node")
1147+    def _on_create(self, request):
1148 
1149         def toResponse(result):
1150-            if not nodeIdentifier or nodeIdentifier != result:
1151+            if not request.nodeIdentifier or request.nodeIdentifier != result:
1152                 response = domish.Element((NS_PUBSUB, 'pubsub'))
1153                 create = response.addElement('create')
1154                 create['node'] = result
[5]1155@@ -751,7 +946,7 @@
[1]1156             else:
1157                 return None
1158 
1159-        d = self.create(requestor, service, nodeIdentifier)
1160+        d = self.create(request)
1161         d.addCallback(toResponse)
1162         return d
1163 
[5]1164@@ -771,6 +966,7 @@
[1]1165             fields.append(data_form.Field.fromDict(option))
1166         return fields
1167 
1168+
1169     def _formFromConfiguration(self, values):
1170         options = self.getConfigurationOptions()
1171         fields = self._makeFields(options, values)
[5]1172@@ -780,6 +976,7 @@
[1]1173 
1174         return form
[4]1175 
[1]1176+
1177     def _checkConfiguration(self, values):
1178         options = self.getConfigurationOptions()
[4]1179         processedValues = {}
[5]1180@@ -805,8 +1002,7 @@
[1]1181         return processedValues
1182 
1183 
1184-    def _onDefault(self, iq):
1185-        requestor, service = self._getParameters(iq)
1186+    def _on_default(self, request):
1187 
1188         def toResponse(options):
1189             response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
[5]1190@@ -814,127 +1010,82 @@
[1]1191             default.addChild(self._formFromConfiguration(options).toElement())
1192             return response
1193 
1194-        form = self._findForm(iq.pubsub.config, NS_PUBSUB_NODE_CONFIG)
1195-        values = form and form.formType == 'result' and form.getValues() or {}
1196-        nodeType = values.get('pubsub#node_type', 'leaf')
1197-
1198-        if nodeType not in ('leaf', 'collections'):
1199+        if request.nodeType not in ('leaf', 'collection'):
1200             return defer.fail(error.StanzaError('not-acceptable'))
1201 
1202-        d = self.getDefaultConfiguration(requestor, service, nodeType)
1203+        d = self.getDefaultConfiguration(request)
1204         d.addCallback(toResponse)
1205         return d
1206 
1207 
1208-    def _onConfigureGet(self, iq):
1209-        requestor, service, nodeIdentifier = self._getParameters(
1210-                iq, 'configure', 'nodeOrEmpty')
1211-
1212+    def _on_configureGet(self, request):
1213         def toResponse(options):
1214             response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
1215             configure = response.addElement("configure")
1216-            configure.addChild(self._formFromConfiguration(options).toElement())
1217+            form = self._formFromConfiguration(options)
1218+            configure.addChild(form.toElement())
1219 
1220-            if nodeIdentifier:
1221-                configure["node"] = nodeIdentifier
1222+            if request.nodeIdentifier:
1223+                configure["node"] = request.nodeIdentifier
1224 
1225             return response
1226 
1227-        d = self.getConfiguration(requestor, service, nodeIdentifier)
1228+        d = self.getConfiguration(request)
1229         d.addCallback(toResponse)
1230         return d
1231 
1232 
1233-    def _onConfigureSet(self, iq):
1234-        requestor, service, nodeIdentifier = self._getParameters(
1235-                iq, 'configure', 'nodeOrEmpty')
1236+    def _on_configureSet(self, request):
1237+        if request.options:
1238+            request.options = self._checkConfiguration(request.options)
1239+            return self.setConfiguration(request)
1240+        else:
1241+            return None
1242 
1243-        # Search configuration form with correct FORM_TYPE and process it
1244 
1245-        form = self._findForm(iq.pubsub.configure, NS_PUBSUB_NODE_CONFIG)
1246 
1247-        if form:
1248-            if form.formType == 'submit':
1249-                options = self._checkConfiguration(form.getValues())
1250-
1251-                return self.setConfiguration(requestor, service,
1252-                                             nodeIdentifier, options)
1253-            elif form.formType == 'cancel':
1254-                return None
1255-
1256-        raise BadRequest()
1257-
1258-
1259-    def _onItems(self, iq):
1260-        requestor, service, nodeIdentifier, maxItems = self._getParameters(
1261-                iq, 'items', 'nodeOrEmpty', 'max_items')
1262-
1263-        itemIdentifiers = []
1264-        for child in iq.pubsub.items.elements():
1265-            if child.name == 'item' and child.uri == NS_PUBSUB:
1266-                try:
1267-                    itemIdentifiers.append(child["id"])
1268-                except KeyError:
1269-                    raise BadRequest()
1270+    def _on_items(self, request):
1271 
1272         def toResponse(result):
1273             response = domish.Element((NS_PUBSUB, 'pubsub'))
1274             items = response.addElement('items')
1275-            if nodeIdentifier:
1276-                items["node"] = nodeIdentifier
1277+            items["node"] = request.nodeIdentifier
1278 
1279             for item in result:
1280                 items.addChild(item)
1281 
1282             return response
1283 
1284-        d = self.items(requestor, service, nodeIdentifier, maxItems,
1285-                       itemIdentifiers)
1286+        d = self.items(request)
1287         d.addCallback(toResponse)
1288         return d
1289 
1290 
1291-    def _onRetract(self, iq):
1292-        requestor, service, nodeIdentifier = self._getParameters(
1293-                iq, 'retract', 'node')
1294+    def _on_retract(self, request):
1295+        return self.retract(request)
1296 
1297-        itemIdentifiers = []
1298-        for child in iq.pubsub.retract.elements():
1299-            if child.uri == NS_PUBSUB and child.name == 'item':
1300-                try:
1301-                    itemIdentifiers.append(child["id"])
1302-                except KeyError:
1303-                    raise BadRequest()
1304 
1305-        return self.retract(requestor, service, nodeIdentifier,
1306-                            itemIdentifiers)
1307+    def _on_purge(self, request):
1308+        return self.purge(request)
1309 
1310 
1311-    def _onPurge(self, iq):
1312-        requestor, service, nodeIdentifier = self._getParameters(
1313-                iq, 'purge', 'node')
1314-        return self.purge(requestor, service, nodeIdentifier)
1315+    def _on_delete(self, request):
1316+        return self.delete(request)
1317 
1318 
1319-    def _onDelete(self, iq):
1320-        requestor, service, nodeIdentifier = self._getParameters(
1321-                iq, 'delete', 'node')
1322-        return self.delete(requestor, service, nodeIdentifier)
1323-
1324-
1325-    def _onAffiliationsGet(self, iq):
1326+    def _on_affiliationsGet(self, iq):
1327         raise Unsupported('modify-affiliations')
1328 
1329 
1330-    def _onAffiliationsSet(self, iq):
1331+    def _on_affiliationsSet(self, iq):
1332         raise Unsupported('modify-affiliations')
1333 
1334 
1335-    def _onSubscriptionsGet(self, iq):
1336+    def _on_subscriptionsGet(self, iq):
1337         raise Unsupported('manage-subscriptions')
1338 
1339 
1340-    def _onSubscriptionsSet(self, iq):
1341+    def _on_subscriptionsSet(self, iq):
1342         raise Unsupported('manage-subscriptions')
1343 
1344     # public methods
[5]1345@@ -990,27 +1141,27 @@
[1]1346         return []
1347 
1348 
1349-    def publish(self, requestor, service, nodeIdentifier, items):
1350+    def publish(self, request):
1351         raise Unsupported('publish')
1352 
1353 
1354-    def subscribe(self, requestor, service, nodeIdentifier, subscriber):
1355+    def subscribe(self, request):
1356         raise Unsupported('subscribe')
1357 
1358 
1359-    def unsubscribe(self, requestor, service, nodeIdentifier, subscriber):
1360+    def unsubscribe(self, request):
1361         raise Unsupported('subscribe')
1362 
1363 
1364-    def subscriptions(self, requestor, service):
1365+    def subscriptions(self, request):
1366         raise Unsupported('retrieve-subscriptions')
1367 
1368 
1369-    def affiliations(self, requestor, service):
1370+    def affiliations(self, request):
1371         raise Unsupported('retrieve-affiliations')
1372 
1373 
1374-    def create(self, requestor, service, nodeIdentifier):
1375+    def create(self, request):
1376         raise Unsupported('create-nodes')
1377 
1378 
[5]1379@@ -1018,30 +1169,29 @@
[1]1380         return {}
1381 
1382 
1383-    def getDefaultConfiguration(self, requestor, service, nodeType):
1384+    def getDefaultConfiguration(self, request):
1385         raise Unsupported('retrieve-default')
1386 
1387 
1388-    def getConfiguration(self, requestor, service, nodeIdentifier):
1389+    def getConfiguration(self, request):
1390         raise Unsupported('config-node')
1391 
1392 
1393-    def setConfiguration(self, requestor, service, nodeIdentifier, options):
1394+    def setConfiguration(self, request):
1395         raise Unsupported('config-node')
1396 
1397 
1398-    def items(self, requestor, service, nodeIdentifier, maxItems,
1399-                    itemIdentifiers):
1400+    def items(self, request):
1401         raise Unsupported('retrieve-items')
1402 
1403 
1404-    def retract(self, requestor, service, nodeIdentifier, itemIdentifiers):
1405+    def retract(self, request):
1406         raise Unsupported('retract-items')
1407 
1408 
1409-    def purge(self, requestor, service, nodeIdentifier):
1410+    def purge(self, request):
1411         raise Unsupported('purge-nodes')
1412 
1413 
1414-    def delete(self, requestor, service, nodeIdentifier):
1415+    def delete(self, request):
1416         raise Unsupported('delete-nodes')
[7]1417diff -r 556fc011f965 wokkel/test/test_pubsub.py
1418--- a/wokkel/test/test_pubsub.py        Tue Apr 07 11:16:01 2009 +0200
1419+++ b/wokkel/test/test_pubsub.py        Tue Apr 07 11:17:21 2009 +0200
1420@@ -15,6 +15,7 @@
1421 from twisted.words.protocols.jabber.xmlstream import toResponse
[1]1422 
[7]1423 from wokkel import data_form, disco, iwokkel, pubsub, shim
[5]1424+from wokkel.generic import parseXml
1425 from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
[1]1426 
[7]1427 NS_PUBSUB = 'http://jabber.org/protocol/pubsub'
1428@@ -487,6 +488,630 @@
[5]1429 
1430 
1431 
1432+class PubSubRequestTest(unittest.TestCase):
1433+
1434+    def test_fromElementPublish(self):
[1]1435+        """
[5]1436+        Test parsing a publish request.
[1]1437+        """
[5]1438+
1439+        xml = """
1440+        <iq type='set' to='pubsub.example.org'
1441+                       from='user@example.org'>
1442+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1443+            <publish node='test'/>
1444+          </pubsub>
1445+        </iq>
[1]1446+        """
[5]1447+
1448+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1449+        self.assertEqual('publish', request.verb)
1450+        self.assertEqual(JID('user@example.org'), request.sender)
1451+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
1452+        self.assertEqual('test', request.nodeIdentifier)
1453+        self.assertEqual([], request.items)
1454+
1455+
1456+    def test_fromElementPublishItems(self):
1457+        """
1458+        Test parsing a publish request with items.
[1]1459+        """
1460+
1461+        xml = """
1462+        <iq type='set' to='pubsub.example.org'
1463+                       from='user@example.org'>
1464+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1465+            <publish node='test'>
1466+              <item id="item1"/>
1467+              <item id="item2"/>
1468+            </publish>
1469+          </pubsub>
1470+        </iq>
1471+        """
1472+
[5]1473+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1474+        self.assertEqual(2, len(request.items))
1475+        self.assertEqual(u'item1', request.items[0]["id"])
1476+        self.assertEqual(u'item2', request.items[1]["id"])
1477+
1478+
1479+    def test_fromElementPublishNoNode(self):
[1]1480+        """
[5]1481+        A publish request to the root node should raise an exception.
1482+        """
1483+        xml = """
1484+        <iq type='set' to='pubsub.example.org'
1485+                       from='user@example.org'>
1486+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1487+            <publish/>
1488+          </pubsub>
1489+        </iq>
1490+        """
1491+
1492+        err = self.assertRaises(error.StanzaError,
1493+                                pubsub.PubSubRequest.fromElement,
1494+                                parseXml(xml))
1495+        self.assertEqual('bad-request', err.condition)
1496+        self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri)
1497+        self.assertEqual('nodeid-required', err.appCondition.name)
1498+
1499+
1500+    def test_fromElementSubscribe(self):
1501+        """
1502+        Test parsing a subscription request.
[1]1503+        """
1504+
1505+        xml = """
1506+        <iq type='set' to='pubsub.example.org'
1507+                       from='user@example.org'>
1508+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1509+            <subscribe node='test' jid='user@example.org/Home'/>
1510+          </pubsub>
1511+        </iq>
1512+        """
1513+
[5]1514+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1515+        self.assertEqual('subscribe', request.verb)
1516+        self.assertEqual(JID('user@example.org'), request.sender)
1517+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
1518+        self.assertEqual('test', request.nodeIdentifier)
1519+        self.assertEqual(JID('user@example.org/Home'), request.subscriber)
1520+
1521+
1522+    def test_fromElementSubscribeEmptyNode(self):
[1]1523+        """
[5]1524+        Test parsing a subscription request to the root node.
[1]1525+        """
1526+
1527+        xml = """
1528+        <iq type='set' to='pubsub.example.org'
1529+                       from='user@example.org'>
1530+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1531+            <subscribe jid='user@example.org/Home'/>
1532+          </pubsub>
1533+        </iq>
1534+        """
1535+
[5]1536+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1537+        self.assertEqual('', request.nodeIdentifier)
1538+
1539+
1540+    def test_fromElementSubscribeNoJID(self):
[1]1541+        """
[5]1542+        Subscribe requests without a JID should raise a bad-request exception.
1543+        """
1544+        xml = """
1545+        <iq type='set' to='pubsub.example.org'
1546+                       from='user@example.org'>
1547+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1548+            <subscribe node='test'/>
1549+          </pubsub>
1550+        </iq>
1551+        """
1552+        err = self.assertRaises(error.StanzaError,
1553+                                pubsub.PubSubRequest.fromElement,
1554+                                parseXml(xml))
1555+        self.assertEqual('bad-request', err.condition)
1556+        self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri)
1557+        self.assertEqual('jid-required', err.appCondition.name)
1558+
1559+    def test_fromElementUnsubscribe(self):
1560+        """
1561+        Test parsing an unsubscription request.
[1]1562+        """
1563+
1564+        xml = """
1565+        <iq type='set' to='pubsub.example.org'
1566+                       from='user@example.org'>
1567+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1568+            <unsubscribe node='test' jid='user@example.org/Home'/>
1569+          </pubsub>
1570+        </iq>
1571+        """
1572+
[5]1573+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1574+        self.assertEqual('unsubscribe', request.verb)
1575+        self.assertEqual(JID('user@example.org'), request.sender)
1576+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
1577+        self.assertEqual('test', request.nodeIdentifier)
1578+        self.assertEqual(JID('user@example.org/Home'), request.subscriber)
1579+
1580+
1581+    def test_fromElementUnsubscribeNoJID(self):
[4]1582+        """
[5]1583+        Unsubscribe requests without a JID should raise a bad-request exception.
[4]1584+        """
1585+        xml = """
1586+        <iq type='set' to='pubsub.example.org'
[1]1587+                       from='user@example.org'>
1588+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
[5]1589+            <unsubscribe node='test'/>
[1]1590+          </pubsub>
1591+        </iq>
1592+        """
[5]1593+        err = self.assertRaises(error.StanzaError,
1594+                                pubsub.PubSubRequest.fromElement,
1595+                                parseXml(xml))
1596+        self.assertEqual('bad-request', err.condition)
1597+        self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri)
1598+        self.assertEqual('jid-required', err.appCondition.name)
1599+
1600+
1601+    def test_fromElementOptionsGet(self):
[1]1602+        """
[5]1603+        Test parsing a request for getting subscription options.
1604+        """
1605+
1606+        xml = """
1607+        <iq type='get' to='pubsub.example.org'
1608+                       from='user@example.org'>
1609+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1610+            <options node='test' jid='user@example.org/Home'/>
1611+          </pubsub>
1612+        </iq>
1613+        """
1614+
1615+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1616+        self.assertEqual('optionsGet', request.verb)
1617+
1618+
1619+    def test_fromElementOptionsSet(self):
1620+        """
1621+        Test parsing a request for setting subscription options.
1622+        """
1623+
1624+        xml = """
1625+        <iq type='set' to='pubsub.example.org'
1626+                       from='user@example.org'>
1627+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1628+            <options node='test' jid='user@example.org/Home'>
1629+              <x xmlns='jabber:x:data' type='submit'>
1630+                <field var='FORM_TYPE' type='hidden'>
1631+                  <value>http://jabber.org/protocol/pubsub#subscribe_options</value>
1632+                </field>
1633+                <field var='pubsub#deliver'><value>1</value></field>
1634+              </x>
1635+            </options>
1636+          </pubsub>
1637+        </iq>
1638+        """
1639+
1640+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1641+        self.assertEqual('optionsSet', request.verb)
1642+        self.assertEqual(JID('user@example.org'), request.sender)
1643+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
1644+        self.assertEqual('test', request.nodeIdentifier)
1645+        self.assertEqual(JID('user@example.org/Home'), request.subscriber)
1646+        self.assertEqual({'pubsub#deliver': '1'}, request.options)
1647+
1648+
1649+    def test_fromElementOptionsSetCancel(self):
1650+        """
1651+        Test parsing a request for cancelling setting subscription options.
1652+        """
1653+
1654+        xml = """
1655+        <iq type='set' to='pubsub.example.org'
1656+                       from='user@example.org'>
1657+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1658+            <options node='test' jid='user@example.org/Home'>
1659+              <x xmlns='jabber:x:data' type='cancel'/>
1660+            </options>
1661+          </pubsub>
1662+        </iq>
1663+        """
1664+
1665+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1666+        self.assertEqual({}, request.options)
1667+
1668+
1669+    def test_fromElementOptionsSetBadFormType(self):
1670+        """
1671+        On a options set request unknown fields should be ignored.
1672+        """
1673+
1674+        xml = """
1675+        <iq type='set' to='pubsub.example.org'
1676+                       from='user@example.org'>
1677+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1678+            <options node='test' jid='user@example.org/Home'>
1679+              <x xmlns='jabber:x:data' type='result'>
1680+                <field var='FORM_TYPE' type='hidden'>
1681+                  <value>http://jabber.org/protocol/pubsub#node_config</value>
1682+                </field>
1683+                <field var='pubsub#deliver'><value>1</value></field>
1684+              </x>
1685+            </options>
1686+          </pubsub>
1687+        </iq>
1688+        """
1689+
1690+        err = self.assertRaises(error.StanzaError,
1691+                                pubsub.PubSubRequest.fromElement,
1692+                                parseXml(xml))
1693+        self.assertEqual('bad-request', err.condition)
1694+        self.assertEqual(None, err.appCondition)
1695+
1696+
1697+    def test_fromElementOptionsSetNoForm(self):
1698+        """
1699+        On a options set request a form is required.
1700+        """
1701+
1702+        xml = """
1703+        <iq type='set' to='pubsub.example.org'
1704+                       from='user@example.org'>
1705+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1706+            <options node='test' jid='user@example.org/Home'/>
1707+          </pubsub>
1708+        </iq>
1709+        """
1710+        err = self.assertRaises(error.StanzaError,
1711+                                pubsub.PubSubRequest.fromElement,
1712+                                parseXml(xml))
1713+        self.assertEqual('bad-request', err.condition)
1714+        self.assertEqual(None, err.appCondition)
1715+
1716+
1717+    def test_fromElementSubscriptions(self):
1718+        """
1719+        Test parsing a request for all subscriptions.
1720+        """
1721+
1722+        xml = """
1723+        <iq type='get' to='pubsub.example.org'
1724+                       from='user@example.org'>
1725+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1726+            <subscriptions/>
1727+          </pubsub>
1728+        </iq>
1729+        """
1730+
1731+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1732+        self.assertEqual('subscriptions', request.verb)
1733+        self.assertEqual(JID('user@example.org'), request.sender)
1734+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
1735+
1736+
1737+    def test_fromElementAffiliations(self):
1738+        """
1739+        Test parsing a request for all affiliations.
[1]1740+        """
1741+
1742+        xml = """
1743+        <iq type='get' to='pubsub.example.org'
1744+                       from='user@example.org'>
1745+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1746+            <affiliations/>
1747+          </pubsub>
1748+        </iq>
1749+        """
1750+
[5]1751+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1752+        self.assertEqual('affiliations', request.verb)
1753+        self.assertEqual(JID('user@example.org'), request.sender)
1754+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
1755+
1756+
1757+    def test_fromElementCreate(self):
[1]1758+        """
[5]1759+        Test parsing a request to create a node.
[1]1760+        """
1761+
1762+        xml = """
1763+        <iq type='set' to='pubsub.example.org'
1764+                       from='user@example.org'>
1765+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1766+            <create node='mynode'/>
1767+          </pubsub>
1768+        </iq>
1769+        """
1770+
[5]1771+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1772+        self.assertEqual('create', request.verb)
1773+        self.assertEqual(JID('user@example.org'), request.sender)
1774+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
1775+        self.assertEqual('mynode', request.nodeIdentifier)
1776+
1777+
1778+    def test_fromElementCreateInstant(self):
[1]1779+        """
[5]1780+        Test parsing a request to create an instant node.
[1]1781+        """
1782+
1783+        xml = """
1784+        <iq type='set' to='pubsub.example.org'
1785+                       from='user@example.org'>
1786+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1787+            <create/>
1788+          </pubsub>
1789+        </iq>
1790+        """
1791+
[5]1792+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1793+        self.assertIdentical(None, request.nodeIdentifier)
1794+
1795+
1796+    def test_fromElementDefault(self):
[1]1797+        """
[5]1798+        Test parsing a request for the default node configuration.
1799+        """
1800+
1801+        xml = """
1802+        <iq type='get' to='pubsub.example.org'
1803+                       from='user@example.org'>
1804+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1805+            <default/>
1806+          </pubsub>
1807+        </iq>
1808+        """
1809+
1810+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1811+        self.assertEqual('default', request.verb)
1812+        self.assertEqual(JID('user@example.org'), request.sender)
1813+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
1814+        self.assertEqual('leaf', request.nodeType)
1815+
1816+
1817+    def test_fromElementDefaultCollection(self):
1818+        """
1819+        Parsing a request for the default configuration extracts the node type.
[1]1820+        """
1821+
1822+        xml = """
1823+        <iq type='get' to='pubsub.example.org'
1824+                       from='user@example.org'>
1825+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1826+            <default>
1827+              <x xmlns='jabber:x:data' type='submit'>
1828+                <field var='FORM_TYPE' type='hidden'>
1829+                  <value>http://jabber.org/protocol/pubsub#node_config</value>
1830+                </field>
1831+                <field var='pubsub#node_type'>
1832+                  <value>collection</value>
1833+                </field>
1834+              </x>
1835+            </default>
1836+
1837+          </pubsub>
1838+        </iq>
1839+        """
1840+
[5]1841+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1842+        self.assertEqual('collection', request.nodeType)
1843+
1844+
1845+    def test_fromElementConfigureGet(self):
[1]1846+        """
[5]1847+        Test parsing a node configuration get request.
[1]1848+        """
1849+
1850+        xml = """
1851+        <iq type='get' to='pubsub.example.org'
1852+                       from='user@example.org'>
1853+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
[5]1854+            <configure node='test'/>
1855+          </pubsub>
1856+        </iq>
1857+        """
1858+
1859+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1860+        self.assertEqual('configureGet', request.verb)
1861+        self.assertEqual(JID('user@example.org'), request.sender)
1862+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
1863+        self.assertEqual('test', request.nodeIdentifier)
1864+
1865+
1866+    def test_fromElementConfigureSet(self):
1867+        """
1868+        On a node configuration set request the Data Form is parsed.
1869+        """
1870+
1871+        xml = """
1872+        <iq type='set' to='pubsub.example.org'
1873+                       from='user@example.org'>
1874+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1875+            <configure node='test'>
[1]1876+              <x xmlns='jabber:x:data' type='submit'>
1877+                <field var='FORM_TYPE' type='hidden'>
1878+                  <value>http://jabber.org/protocol/pubsub#node_config</value>
1879+                </field>
[5]1880+                <field var='pubsub#deliver_payloads'><value>0</value></field>
1881+                <field var='pubsub#persist_items'><value>1</value></field>
[1]1882+              </x>
[5]1883+            </configure>
[1]1884+          </pubsub>
1885+        </iq>
1886+        """
1887+
[5]1888+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1889+        self.assertEqual('configureSet', request.verb)
1890+        self.assertEqual(JID('user@example.org'), request.sender)
1891+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
1892+        self.assertEqual('test', request.nodeIdentifier)
1893+        self.assertEqual({'pubsub#deliver_payloads': '0',
1894+                          'pubsub#persist_items': '1'}, request.options)
1895+
1896+
1897+    def test_fromElementConfigureSetCancel(self):
1898+        """
1899+        The node configuration is cancelled, so no options.
1900+        """
1901+
1902+        xml = """
1903+        <iq type='set' to='pubsub.example.org'
1904+                       from='user@example.org'>
1905+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1906+            <configure node='test'>
1907+              <x xmlns='jabber:x:data' type='cancel'/>
1908+            </configure>
1909+          </pubsub>
1910+        </iq>
1911+        """
1912+
1913+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1914+        self.assertEqual({}, request.options)
1915+
1916+
1917+    def test_fromElementConfigureSetBadFormType(self):
[1]1918+        """
1919+        On a node configuration set request unknown fields should be ignored.
1920+        """
1921+
1922+        xml = """
1923+        <iq type='set' to='pubsub.example.org'
1924+                       from='user@example.org'>
1925+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1926+            <configure node='test'>
1927+              <x xmlns='jabber:x:data' type='result'>
1928+                <field var='FORM_TYPE' type='hidden'>
1929+                  <value>http://jabber.org/protocol/pubsub#node_config</value>
1930+                </field>
1931+                <field var='pubsub#deliver_payloads'><value>0</value></field>
1932+                <field var='x-myfield'><value>1</value></field>
1933+              </x>
1934+            </configure>
1935+          </pubsub>
1936+        </iq>
1937+        """
1938+
[5]1939+        err = self.assertRaises(error.StanzaError,
1940+                                pubsub.PubSubRequest.fromElement,
1941+                                parseXml(xml))
1942+        self.assertEqual('bad-request', err.condition)
1943+        self.assertEqual(None, err.appCondition)
1944+
1945+
1946+    def test_fromElementConfigureSetNoForm(self):
1947+        """
1948+        On a node configuration set request a form is required.
1949+        """
1950+
1951+        xml = """
1952+        <iq type='set' to='pubsub.example.org'
1953+                       from='user@example.org'>
1954+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1955+            <configure node='test'/>
1956+          </pubsub>
1957+        </iq>
1958+        """
1959+        err = self.assertRaises(error.StanzaError,
1960+                                pubsub.PubSubRequest.fromElement,
1961+                                parseXml(xml))
1962+        self.assertEqual('bad-request', err.condition)
1963+        self.assertEqual(None, err.appCondition)
1964+
1965+
1966+    def test_fromElementItems(self):
1967+        """
1968+        Test parsing an items request.
1969+        """
1970+        xml = """
1971+        <iq type='get' to='pubsub.example.org'
1972+                       from='user@example.org'>
1973+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1974+            <items node='test'/>
1975+          </pubsub>
1976+        </iq>
1977+        """
1978+
1979+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
1980+        self.assertEqual('items', request.verb)
1981+        self.assertEqual(JID('user@example.org'), request.sender)
1982+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
1983+        self.assertEqual('test', request.nodeIdentifier)
1984+        self.assertIdentical(None, request.maxItems)
1985+        self.assertEqual([], request.itemIdentifiers)
1986+
1987+
1988+    def test_fromElementRetract(self):
1989+        """
1990+        Test parsing a retract request.
1991+        """
1992+
1993+        xml = """
1994+        <iq type='set' to='pubsub.example.org'
1995+                       from='user@example.org'>
1996+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1997+            <retract node='test'>
1998+              <item id='item1'/>
1999+              <item id='item2'/>
2000+            </retract>
2001+          </pubsub>
2002+        </iq>
2003+        """
2004+
2005+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
2006+        self.assertEqual('retract', request.verb)
2007+        self.assertEqual(JID('user@example.org'), request.sender)
2008+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
2009+        self.assertEqual('test', request.nodeIdentifier)
2010+        self.assertEqual(['item1', 'item2'], request.itemIdentifiers)
2011+
2012+
2013+    def test_fromElementPurge(self):
2014+        """
2015+        Test parsing a purge request.
2016+        """
2017+
2018+        xml = """
2019+        <iq type='set' to='pubsub.example.org'
2020+                       from='user@example.org'>
2021+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2022+            <purge node='test'/>
2023+          </pubsub>
2024+        </iq>
2025+        """
2026+
2027+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
2028+        self.assertEqual('purge', request.verb)
2029+        self.assertEqual(JID('user@example.org'), request.sender)
2030+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
2031+        self.assertEqual('test', request.nodeIdentifier)
2032+
2033+
2034+    def test_fromElementDelete(self):
2035+        """
2036+        Test parsing a delete request.
2037+        """
2038+
2039+        xml = """
2040+        <iq type='set' to='pubsub.example.org'
2041+                       from='user@example.org'>
2042+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2043+            <delete node='test'/>
2044+          </pubsub>
2045+        </iq>
2046+        """
2047+
2048+        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
2049+        self.assertEqual('delete', request.verb)
2050+        self.assertEqual(JID('user@example.org'), request.sender)
2051+        self.assertEqual(JID('pubsub.example.org'), request.recipient)
2052+        self.assertEqual('test', request.nodeIdentifier)
2053+
2054+
2055+
2056 class PubSubServiceTest(unittest.TestCase, TestableRequestHandlerMixin):
2057     """
2058     Tests for L{pubsub.PubSubService}.
[7]2059@@ -504,6 +1129,29 @@
[5]2060         verify.verifyObject(iwokkel.IPubSubService, self.service)
2061 
2062 
2063+    def test_connectionMade(self):
2064+        """
2065+        Verify setup of observers in L{pubsub.connectionMade}.
2066+        """
2067+        requests = []
2068+
2069+        def handleRequest(iq):
2070+            requests.append(iq)
2071+
2072+        self.service.xmlstream = self.stub.xmlstream
2073+        self.service.handleRequest = handleRequest
2074+        self.service.connectionMade()
2075+
2076+        for namespace in (NS_PUBSUB, NS_PUBSUB_OWNER):
2077+            for stanzaType in ('get', 'set'):
2078+                iq = domish.Element((None, 'iq'))
2079+                iq['type'] = stanzaType
2080+                iq.addElement((namespace, 'pubsub'))
2081+                self.stub.xmlstream.dispatch(iq)
2082+
2083+        self.assertEqual(4, len(requests))
2084+
2085+
2086     def test_getDiscoInfo(self):
2087         """
2088         Test getDiscoInfo calls getNodeInfo and returns some minimal info.
[7]2089@@ -569,28 +1217,6 @@
[5]2090         return d
2091 
2092 
2093-    def test_onPublishNoNode(self):
2094-        """
2095-        The root node is always a collection, publishing is a bad request.
2096-        """
2097-        xml = """
2098-        <iq type='set' to='pubsub.example.org'
2099-                       from='user@example.org'>
2100-          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2101-            <publish/>
2102-          </pubsub>
2103-        </iq>
2104-        """
2105-
2106-        def cb(result):
2107-            self.assertEquals('bad-request', result.condition)
2108-
2109-        d = self.handleRequest(xml)
2110-        self.assertFailure(d, error.StanzaError)
2111-        d.addCallback(cb)
2112-        return d
2113-
2114-
2115     def test_onPublish(self):
2116         """
2117         A publish request should result in L{PubSubService.publish} being
[7]2118@@ -606,27 +1232,147 @@
[5]2119         </iq>
2120         """
2121 
2122-        def publish(requestor, service, nodeIdentifier, items):
2123-            self.assertEqual(JID('user@example.org'), requestor)
2124-            self.assertEqual(JID('pubsub.example.org'), service)
2125-            self.assertEqual('test', nodeIdentifier)
2126-            self.assertEqual([], items)
2127+        def publish(request):
2128             return defer.succeed(None)
2129 
2130         self.service.publish = publish
2131+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2132         return self.handleRequest(xml)
2133 
2134 
2135+    def test_onSubscribe(self):
2136+        """
2137+        A successful subscription should return the current subscription.
2138+        """
2139+
2140+        xml = """
2141+        <iq type='set' to='pubsub.example.org'
2142+                       from='user@example.org'>
2143+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2144+            <subscribe node='test' jid='user@example.org/Home'/>
2145+          </pubsub>
2146+        </iq>
2147+        """
2148+
2149+        def subscribe(request):
2150+            return defer.succeed(pubsub.Subscription(request.nodeIdentifier,
2151+                                                     request.subscriber,
2152+                                                     'subscribed'))
2153+
2154+        def cb(element):
2155+            self.assertEqual('pubsub', element.name)
2156+            self.assertEqual(NS_PUBSUB, element.uri)
2157+            subscription = element.subscription
2158+            self.assertEqual(NS_PUBSUB, subscription.uri)
2159+            self.assertEqual('test', subscription['node'])
2160+            self.assertEqual('user@example.org/Home', subscription['jid'])
2161+            self.assertEqual('subscribed', subscription['subscription'])
2162+
2163+        self.service.subscribe = subscribe
2164+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2165+        d = self.handleRequest(xml)
2166+        d.addCallback(cb)
2167+        return d
2168+
2169+
2170+    def test_onSubscribeEmptyNode(self):
2171+        """
2172+        A successful subscription on root node should return no node attribute.
2173+        """
2174+
2175+        xml = """
2176+        <iq type='set' to='pubsub.example.org'
2177+                       from='user@example.org'>
2178+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2179+            <subscribe jid='user@example.org/Home'/>
2180+          </pubsub>
2181+        </iq>
2182+        """
2183+
2184+        def subscribe(request):
2185+            return defer.succeed(pubsub.Subscription(request.nodeIdentifier,
2186+                                                     request.subscriber,
2187+                                                     'subscribed'))
2188+
2189+        def cb(element):
2190+            self.assertFalse(element.subscription.hasAttribute('node'))
2191+
2192+        self.service.subscribe = subscribe
2193+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2194+        d = self.handleRequest(xml)
2195+        d.addCallback(cb)
2196+        return d
2197+
2198+
2199+    def test_onUnsubscribe(self):
2200+        """
2201+        A successful unsubscription should return an empty response.
2202+        """
2203+
2204+        xml = """
2205+        <iq type='set' to='pubsub.example.org'
2206+                       from='user@example.org'>
2207+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2208+            <unsubscribe node='test' jid='user@example.org/Home'/>
2209+          </pubsub>
2210+        </iq>
2211+        """
2212+
2213+        def unsubscribe(request):
2214+            return defer.succeed(None)
2215+
2216+        def cb(element):
2217+            self.assertIdentical(None, element)
2218+
2219+        self.service.unsubscribe = unsubscribe
2220+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2221+        d = self.handleRequest(xml)
2222+        d.addCallback(cb)
2223+        return d
2224+
2225+
2226     def test_onOptionsGet(self):
2227         """
2228-        Subscription options are not supported.
2229+        Getting subscription options is not supported.
2230         """
2231 
2232         xml = """
2233         <iq type='get' to='pubsub.example.org'
2234                        from='user@example.org'>
2235           <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2236-            <options/>
2237+            <options node='test' jid='user@example.org/Home'/>
2238+          </pubsub>
2239+        </iq>
2240+        """
2241+
[1]2242+        def cb(result):
[5]2243+            self.assertEquals('feature-not-implemented', result.condition)
2244+            self.assertEquals('unsupported', result.appCondition.name)
2245+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
[1]2246+
2247+        d = self.handleRequest(xml)
2248+        self.assertFailure(d, error.StanzaError)
2249+        d.addCallback(cb)
2250+        return d
[4]2251+
2252+
[5]2253+    def test_onOptionsSet(self):
2254+        """
2255+        Setting subscription options is not supported.
2256+        """
2257+
2258+        xml = """
2259+        <iq type='set' to='pubsub.example.org'
2260+                       from='user@example.org'>
2261+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2262+            <options node='test' jid='user@example.org/Home'>
2263+              <x xmlns='jabber:x:data' type='submit'>
2264+                <field var='FORM_TYPE' type='hidden'>
2265+                  <value>http://jabber.org/protocol/pubsub#subscribe_options</value>
2266+                </field>
2267+                <field var='pubsub#deliver'><value>1</value></field>
2268+              </x>
2269+            </options>
2270           </pubsub>
2271         </iq>
2272         """
[7]2273@@ -672,14 +1418,141 @@
[5]2274             self.assertEqual('subscribed', subscription['subscription'])
2275 
2276 
2277-        def subscriptions(requestor, service):
2278-            self.assertEqual(JID('user@example.org'), requestor)
2279-            self.assertEqual(JID('pubsub.example.org'), service)
2280+        def subscriptions(request):
2281             subscription = pubsub.Subscription('test', JID('user@example.org'),
2282                                                'subscribed')
2283             return defer.succeed([subscription])
2284 
2285         self.service.subscriptions = subscriptions
2286+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2287+        d = self.handleRequest(xml)
2288+        d.addCallback(cb)
2289+        return d
2290+
2291+
2292+    def test_onAffiliations(self):
2293+        """
2294+        A subscriptions request should result in
2295+        L{PubSubService.affiliations} being called and the result prepared
2296+        for the response.
2297+        """
2298+
2299+        xml = """
2300+        <iq type='get' to='pubsub.example.org'
2301+                       from='user@example.org'>
2302+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2303+            <affiliations/>
2304+          </pubsub>
2305+        </iq>
2306+        """
2307+
2308+        def cb(element):
2309+            self.assertEqual('pubsub', element.name)
2310+            self.assertEqual(NS_PUBSUB, element.uri)
2311+            self.assertEqual(NS_PUBSUB, element.affiliations.uri)
2312+            children = list(element.affiliations.elements())
2313+            self.assertEqual(1, len(children))
2314+            affiliation = children[0]
2315+            self.assertEqual('affiliation', affiliation.name)
2316+            self.assertEqual(NS_PUBSUB, affiliation.uri)
2317+            self.assertEqual('test', affiliation['node'])
2318+            self.assertEqual('owner', affiliation['affiliation'])
2319+
2320+
2321+        def affiliations(request):
2322+            affiliation = ('test', 'owner')
2323+            return defer.succeed([affiliation])
2324+
2325+        self.service.affiliations = affiliations
2326+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2327+        d = self.handleRequest(xml)
2328+        d.addCallback(cb)
2329+        return d
2330+
2331+
2332+    def test_onCreate(self):
2333+        """
2334+        Replies to create node requests don't return the created node.
2335+        """
2336+
2337+        xml = """
2338+        <iq type='set' to='pubsub.example.org'
2339+                       from='user@example.org'>
2340+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2341+            <create node='mynode'/>
2342+          </pubsub>
2343+        </iq>
2344+        """
2345+
2346+        def create(request):
2347+            return defer.succeed(request.nodeIdentifier)
2348+
2349+        def cb(element):
2350+            self.assertIdentical(None, element)
2351+
2352+        self.service.create = create
2353+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2354+        d = self.handleRequest(xml)
2355+        d.addCallback(cb)
2356+        return d
2357+
2358+
2359+    def test_onCreateChanged(self):
2360+        """
2361+        Replies to create node requests return the created node if changed.
2362+        """
2363+
2364+        xml = """
2365+        <iq type='set' to='pubsub.example.org'
2366+                       from='user@example.org'>
2367+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2368+            <create node='mynode'/>
2369+          </pubsub>
2370+        </iq>
2371+        """
2372+
2373+        def create(request):
2374+            return defer.succeed(u'myrenamednode')
2375+
2376+        def cb(element):
2377+            self.assertEqual('pubsub', element.name)
2378+            self.assertEqual(NS_PUBSUB, element.uri)
2379+            self.assertEqual(NS_PUBSUB, element.create.uri)
2380+            self.assertEqual(u'myrenamednode',
2381+                             element.create.getAttribute('node'))
2382+
2383+        self.service.create = create
2384+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2385+        d = self.handleRequest(xml)
2386+        d.addCallback(cb)
2387+        return d
2388+
2389+
2390+    def test_onCreateInstant(self):
2391+        """
2392+        Replies to create instant node requests return the created node.
2393+        """
2394+
2395+        xml = """
2396+        <iq type='set' to='pubsub.example.org'
2397+                       from='user@example.org'>
2398+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2399+            <create/>
2400+          </pubsub>
2401+        </iq>
2402+        """
2403+
2404+        def create(request):
2405+            return defer.succeed(u'random')
2406+
2407+        def cb(element):
2408+            self.assertEqual('pubsub', element.name)
2409+            self.assertEqual(NS_PUBSUB, element.uri)
2410+            self.assertEqual(NS_PUBSUB, element.create.uri)
2411+            self.assertEqual(u'random', element.create.getAttribute('node'))
2412+
2413+        self.service.create = create
2414+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2415         d = self.handleRequest(xml)
2416         d.addCallback(cb)
2417         return d
[7]2418@@ -710,10 +1583,7 @@
[5]2419                      "label": "Deliver payloads with event notifications"}
2420                 }
2421 
2422-        def getDefaultConfiguration(requestor, service, nodeType):
2423-            self.assertEqual(JID('user@example.org'), requestor)
2424-            self.assertEqual(JID('pubsub.example.org'), service)
2425-            self.assertEqual('leaf', nodeType)
2426+        def getDefaultConfiguration(request):
2427             return defer.succeed({})
2428 
2429         def cb(element):
[7]2430@@ -731,6 +1601,85 @@
[5]2431         return d
2432 
2433 
2434+    def test_onDefaultCollection(self):
2435+        """
2436+        Responses to default requests should depend on passed node type.
2437+        """
2438+
2439+        xml = """
2440+        <iq type='get' to='pubsub.example.org'
2441+                       from='user@example.org'>
2442+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2443+            <default>
2444+              <x xmlns='jabber:x:data' type='submit'>
2445+                <field var='FORM_TYPE' type='hidden'>
2446+                  <value>http://jabber.org/protocol/pubsub#node_config</value>
2447+                </field>
2448+                <field var='pubsub#node_type'>
2449+                  <value>collection</value>
2450+                </field>
2451+              </x>
2452+            </default>
2453+
2454+          </pubsub>
2455+        </iq>
2456+        """
2457+
2458+        def getConfigurationOptions():
2459+            return {
2460+                "pubsub#deliver_payloads":
2461+                    {"type": "boolean",
2462+                     "label": "Deliver payloads with event notifications"}
2463+                }
2464+
2465+        def getDefaultConfiguration(request):
2466+            return defer.succeed({})
2467+
2468+        self.service.getConfigurationOptions = getConfigurationOptions
2469+        self.service.getDefaultConfiguration = getDefaultConfiguration
2470+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2471+        return self.handleRequest(xml)
2472+
2473+
2474+    def test_onDefaultUnknownNodeType(self):
2475+        """
2476+        A default request should result in
2477+        L{PubSubService.getDefaultConfiguration} being called.
2478+        """
2479+
2480+        xml = """
2481+        <iq type='get' to='pubsub.example.org'
2482+                       from='user@example.org'>
2483+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2484+            <default>
2485+              <x xmlns='jabber:x:data' type='submit'>
2486+                <field var='FORM_TYPE' type='hidden'>
2487+                  <value>http://jabber.org/protocol/pubsub#node_config</value>
2488+                </field>
2489+                <field var='pubsub#node_type'>
2490+                  <value>unknown</value>
2491+                </field>
2492+              </x>
2493+            </default>
2494+
2495+          </pubsub>
2496+        </iq>
2497+        """
2498+
2499+        def getDefaultConfiguration(request):
2500+            self.fail("Unexpected call to getConfiguration")
2501+
2502+        def cb(result):
2503+            self.assertEquals('not-acceptable', result.condition)
2504+
2505+        self.service.getDefaultConfiguration = getDefaultConfiguration
2506+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2507+        d = self.handleRequest(xml)
2508+        self.assertFailure(d, error.StanzaError)
2509+        d.addCallback(cb)
2510+        return d
2511+
2512+
2513     def test_onConfigureGet(self):
2514         """
2515         On a node configuration get request L{PubSubService.getConfiguration}
[7]2516@@ -759,14 +1708,11 @@
[5]2517                      "label": "Owner of the node"}
2518                 }
2519 
2520-        def getConfiguration(requestor, service, nodeIdentifier):
2521-            self.assertEqual(JID('user@example.org'), requestor)
2522-            self.assertEqual(JID('pubsub.example.org'), service)
2523-            self.assertEqual('test', nodeIdentifier)
2524-
2525+        def getConfiguration(request):
2526             return defer.succeed({'pubsub#deliver_payloads': '0',
2527                                   'pubsub#persist_items': '1',
2528-                                  'pubsub#owner': JID('user@example.org')})
2529+                                  'pubsub#owner': JID('user@example.org'),
2530+                                  'x-myfield': ['a', 'b']})
2531 
2532         def cb(element):
2533             self.assertEqual('pubsub', element.name)
[7]2534@@ -794,8 +1740,12 @@
[5]2535             field.typeCheck()
2536             self.assertEqual(JID('user@example.org'), field.value)
2537 
2538+            self.assertNotIn('x-myfield', fields)
2539+
2540+
2541         self.service.getConfigurationOptions = getConfigurationOptions
2542         self.service.getConfiguration = getConfiguration
2543+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2544         d = self.handleRequest(xml)
2545         d.addCallback(cb)
2546         return d
[7]2547@@ -834,16 +1784,14 @@
[5]2548                      "label": "Deliver payloads with event notifications"}
2549                 }
2550 
2551-        def setConfiguration(requestor, service, nodeIdentifier, options):
2552-            self.assertEqual(JID('user@example.org'), requestor)
2553-            self.assertEqual(JID('pubsub.example.org'), service)
2554-            self.assertEqual('test', nodeIdentifier)
2555+        def setConfiguration(request):
2556             self.assertEqual({'pubsub#deliver_payloads': False,
2557-                              'pubsub#persist_items': True}, options)
2558+                              'pubsub#persist_items': True}, request.options)
2559             return defer.succeed(None)
2560 
2561         self.service.getConfigurationOptions = getConfigurationOptions
2562         self.service.setConfiguration = setConfiguration
2563+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2564         return self.handleRequest(xml)
2565 
2566 
[7]2567@@ -868,10 +1816,11 @@
[5]2568         </iq>
2569         """
2570 
2571-        def setConfiguration(requestor, service, nodeIdentifier, options):
2572+        def setConfiguration(request):
2573             self.fail("Unexpected call to setConfiguration")
2574 
2575         self.service.setConfiguration = setConfiguration
2576+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2577         return self.handleRequest(xml)
2578 
2579 
[7]2580@@ -907,14 +1856,47 @@
[5]2581                      "label": "Deliver payloads with event notifications"}
2582                 }
2583 
2584-        def setConfiguration(requestor, service, nodeIdentifier, options):
2585-            self.assertEquals(['pubsub#deliver_payloads'], options.keys())
2586+        def setConfiguration(request):
2587+            self.assertEquals(['pubsub#deliver_payloads'],
2588+                              request.options.keys())
2589 
2590         self.service.getConfigurationOptions = getConfigurationOptions
2591         self.service.setConfiguration = setConfiguration
2592+        verify.verifyObject(iwokkel.IPubSubService, self.service)
2593         return self.handleRequest(xml)
2594 
2595 
2596+    def test_onConfigureSetBadFormType(self):
2597+        """
2598+        On a node configuration set request unknown fields should be ignored.
2599+        """
2600+
2601+        xml = """
2602+        <iq type='set' to='pubsub.example.org'
2603+                       from='user@example.org'>
2604+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2605+            <configure node='test'>
2606+              <x xmlns='jabber:x:data' type='result'>
2607+                <field var='FORM_TYPE' type='hidden'>
2608+                  <value>http://jabber.org/protocol/pubsub#node_config</value>
2609+                </field>
2610+                <field var='pubsub#deliver_payloads'><value>0</value></field>
2611+                <field var='x-myfield'><value>1</value></field>
2612+              </x>
2613+            </configure>
2614+          </pubsub>
2615+        </iq>
2616+        """
2617+
2618+        def cb(result):
2619+            self.assertEquals('bad-request', result.condition)
2620+
2621+        d = self.handleRequest(xml)
2622+        self.assertFailure(d, error.StanzaError)
2623+        d.addCallback(cb)
2624+        return d
2625+
2626+
[1]2627     def test_onItems(self):
[4]2628         """
2629         On a items request, return all items for the given node.
[7]2630@@ -928,12 +1910,7 @@
[1]2631         </iq>
2632         """
2633 
2634-        def items(requestor, service, nodeIdentifier, maxItems, items):
2635-            self.assertEqual(JID('user@example.org'), requestor)
2636-            self.assertEqual(JID('pubsub.example.org'), service)
2637-            self.assertEqual('test', nodeIdentifier)
2638-            self.assertIdentical(None, maxItems)
2639-            self.assertEqual([], items)
2640+        def items(request):
2641             return defer.succeed([pubsub.Item('current')])
2642 
2643         def cb(element):
[7]2644@@ -970,11 +1947,7 @@
[1]2645         </iq>
2646         """
2647 
2648-        def retract(requestor, service, nodeIdentifier, itemIdentifiers):
2649-            self.assertEqual(JID('user@example.org'), requestor)
2650-            self.assertEqual(JID('pubsub.example.org'), service)
2651-            self.assertEqual('test', nodeIdentifier)
2652-            self.assertEqual(['item1', 'item2'], itemIdentifiers)
2653+        def retract(request):
2654             return defer.succeed(None)
2655 
2656         self.service.retract = retract
[7]2657@@ -996,10 +1969,7 @@
[1]2658         </iq>
2659         """
2660 
2661-        def purge(requestor, service, nodeIdentifier):
2662-            self.assertEqual(JID('user@example.org'), requestor)
2663-            self.assertEqual(JID('pubsub.example.org'), service)
2664-            self.assertEqual('test', nodeIdentifier)
2665+        def purge(request):
2666             return defer.succeed(None)
2667 
2668         self.service.purge = purge
[7]2669@@ -1021,10 +1991,7 @@
[1]2670         </iq>
2671         """
2672 
2673-        def delete(requestor, service, nodeIdentifier):
2674-            self.assertEqual(JID('user@example.org'), requestor)
2675-            self.assertEqual(JID('pubsub.example.org'), service)
2676-            self.assertEqual('test', nodeIdentifier)
2677+        def delete(request):
2678             return defer.succeed(None)
2679 
2680         self.service.delete = delete
[7]2681@@ -1076,3 +2043,461 @@
[1]2682         self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.redirect.uri)
2683         self.assertTrue(message.event.delete.redirect.hasAttribute('uri'))
2684         self.assertEqual(redirectURI, message.event.delete.redirect['uri'])
2685+
2686+
2687+    def test_onSubscriptionsGet(self):
2688+        """
2689+        Getting subscription options is not supported.
2690+        """
2691+
2692+        xml = """
2693+        <iq type='get' to='pubsub.example.org'
2694+                       from='user@example.org'>
2695+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2696+            <subscriptions/>
2697+          </pubsub>
2698+        </iq>
2699+        """
2700+
2701+        def cb(result):
2702+            self.assertEquals('feature-not-implemented', result.condition)
2703+            self.assertEquals('unsupported', result.appCondition.name)
2704+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2705+            self.assertEquals('manage-subscriptions',
2706+                              result.appCondition['feature'])
2707+
2708+        d = self.handleRequest(xml)
2709+        self.assertFailure(d, error.StanzaError)
2710+        d.addCallback(cb)
2711+        return d
2712+
2713+
2714+    def test_onSubscriptionsSet(self):
2715+        """
2716+        Setting subscription options is not supported.
2717+        """
2718+
2719+        xml = """
2720+        <iq type='set' to='pubsub.example.org'
2721+                       from='user@example.org'>
2722+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2723+            <subscriptions/>
2724+          </pubsub>
2725+        </iq>
2726+        """
2727+
2728+        def cb(result):
2729+            self.assertEquals('feature-not-implemented', result.condition)
2730+            self.assertEquals('unsupported', result.appCondition.name)
2731+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2732+            self.assertEquals('manage-subscriptions',
2733+                              result.appCondition['feature'])
2734+
2735+        d = self.handleRequest(xml)
2736+        self.assertFailure(d, error.StanzaError)
2737+        d.addCallback(cb)
2738+        return d
2739+
2740+
2741+    def test_onAffiliationsGet(self):
2742+        """
2743+        Getting subscription options is not supported.
2744+        """
2745+
2746+        xml = """
2747+        <iq type='get' to='pubsub.example.org'
2748+                       from='user@example.org'>
2749+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2750+            <affiliations/>
2751+          </pubsub>
2752+        </iq>
2753+        """
2754+
2755+        def cb(result):
2756+            self.assertEquals('feature-not-implemented', result.condition)
2757+            self.assertEquals('unsupported', result.appCondition.name)
2758+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2759+            self.assertEquals('modify-affiliations',
2760+                              result.appCondition['feature'])
2761+
2762+        d = self.handleRequest(xml)
2763+        self.assertFailure(d, error.StanzaError)
2764+        d.addCallback(cb)
2765+        return d
2766+
2767+
2768+    def test_onAffiliationsSet(self):
2769+        """
2770+        Setting subscription options is not supported.
2771+        """
2772+
2773+        xml = """
2774+        <iq type='set' to='pubsub.example.org'
2775+                       from='user@example.org'>
2776+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2777+            <affiliations/>
2778+          </pubsub>
2779+        </iq>
2780+        """
2781+
2782+        def cb(result):
2783+            self.assertEquals('feature-not-implemented', result.condition)
2784+            self.assertEquals('unsupported', result.appCondition.name)
2785+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2786+            self.assertEquals('modify-affiliations',
2787+                              result.appCondition['feature'])
2788+
2789+        d = self.handleRequest(xml)
2790+        self.assertFailure(d, error.StanzaError)
2791+        d.addCallback(cb)
2792+        return d
2793+
2794+
2795+    def test_publish(self):
2796+        """
2797+        Non-overridden L{PubSubService.publish} yields unsupported error.
2798+        """
2799+
2800+        xml = """
2801+        <iq type='set' to='pubsub.example.org'
2802+                       from='user@example.org'>
2803+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2804+            <publish node='mynode'/>
2805+          </pubsub>
2806+        </iq>
2807+        """
2808+
2809+        def cb(result):
2810+            self.assertEquals('feature-not-implemented', result.condition)
2811+            self.assertEquals('unsupported', result.appCondition.name)
2812+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2813+            self.assertEquals('publish', result.appCondition['feature'])
2814+
2815+        d = self.handleRequest(xml)
2816+        self.assertFailure(d, error.StanzaError)
2817+        d.addCallback(cb)
2818+        return d
2819+
2820+
2821+    def test_subscribe(self):
2822+        """
2823+        Non-overridden L{PubSubService.subscribe} yields unsupported error.
2824+        """
2825+
2826+        xml = """
2827+        <iq type='set' to='pubsub.example.org'
2828+                       from='user@example.org'>
2829+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2830+            <subscribe node='test' jid='user@example.org/Home'/>
2831+          </pubsub>
2832+        </iq>
2833+        """
2834+
2835+        def cb(result):
2836+            self.assertEquals('feature-not-implemented', result.condition)
2837+            self.assertEquals('unsupported', result.appCondition.name)
2838+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2839+            self.assertEquals('subscribe', result.appCondition['feature'])
2840+
2841+        d = self.handleRequest(xml)
2842+        self.assertFailure(d, error.StanzaError)
2843+        d.addCallback(cb)
2844+        return d
2845+
2846+
2847+    def test_unsubscribe(self):
2848+        """
2849+        Non-overridden L{PubSubService.unsubscribe} yields unsupported error.
2850+        """
2851+
2852+        xml = """
2853+        <iq type='set' to='pubsub.example.org'
2854+                       from='user@example.org'>
2855+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2856+            <unsubscribe node='test' jid='user@example.org/Home'/>
2857+          </pubsub>
2858+        </iq>
2859+        """
2860+
2861+        def cb(result):
2862+            self.assertEquals('feature-not-implemented', result.condition)
2863+            self.assertEquals('unsupported', result.appCondition.name)
2864+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2865+            self.assertEquals('subscribe', result.appCondition['feature'])
2866+
2867+        d = self.handleRequest(xml)
2868+        self.assertFailure(d, error.StanzaError)
2869+        d.addCallback(cb)
2870+        return d
2871+
2872+
2873+    def test_subscriptions(self):
2874+        """
2875+        Non-overridden L{PubSubService.subscriptions} yields unsupported error.
2876+        """
2877+
2878+        xml = """
2879+        <iq type='get' to='pubsub.example.org'
2880+                       from='user@example.org'>
2881+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2882+            <subscriptions/>
2883+          </pubsub>
2884+        </iq>
2885+        """
2886+
2887+        def cb(result):
2888+            self.assertEquals('feature-not-implemented', result.condition)
2889+            self.assertEquals('unsupported', result.appCondition.name)
2890+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2891+            self.assertEquals('retrieve-subscriptions',
2892+                              result.appCondition['feature'])
2893+
2894+        d = self.handleRequest(xml)
2895+        self.assertFailure(d, error.StanzaError)
2896+        d.addCallback(cb)
2897+        return d
2898+
2899+
2900+    def test_affiliations(self):
2901+        """
2902+        Non-overridden L{PubSubService.affiliations} yields unsupported error.
2903+        """
2904+
2905+        xml = """
2906+        <iq type='get' to='pubsub.example.org'
2907+                       from='user@example.org'>
2908+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2909+            <affiliations/>
2910+          </pubsub>
2911+        </iq>
2912+        """
2913+
2914+        def cb(result):
2915+            self.assertEquals('feature-not-implemented', result.condition)
2916+            self.assertEquals('unsupported', result.appCondition.name)
2917+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2918+            self.assertEquals('retrieve-affiliations',
2919+                              result.appCondition['feature'])
2920+
2921+        d = self.handleRequest(xml)
2922+        self.assertFailure(d, error.StanzaError)
2923+        d.addCallback(cb)
2924+        return d
2925+
2926+
2927+    def test_create(self):
2928+        """
2929+        Non-overridden L{PubSubService.create} yields unsupported error.
2930+        """
2931+
2932+        xml = """
2933+        <iq type='set' to='pubsub.example.org'
2934+                       from='user@example.org'>
2935+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2936+            <create node='mynode'/>
2937+          </pubsub>
2938+        </iq>
2939+        """
2940+
2941+        def cb(result):
2942+            self.assertEquals('feature-not-implemented', result.condition)
2943+            self.assertEquals('unsupported', result.appCondition.name)
2944+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2945+            self.assertEquals('create-nodes', result.appCondition['feature'])
2946+
2947+        d = self.handleRequest(xml)
2948+        self.assertFailure(d, error.StanzaError)
2949+        d.addCallback(cb)
2950+        return d
2951+
2952+
2953+    def test_getDefaultConfiguration(self):
2954+        """
2955+        Non-overridden L{PubSubService.getDefaultConfiguration} yields
2956+        unsupported error.
2957+        """
2958+
2959+        xml = """
2960+        <iq type='get' to='pubsub.example.org'
2961+                       from='user@example.org'>
2962+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2963+            <default/>
2964+          </pubsub>
2965+        </iq>
2966+        """
2967+
2968+        def cb(result):
2969+            self.assertEquals('feature-not-implemented', result.condition)
2970+            self.assertEquals('unsupported', result.appCondition.name)
2971+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2972+            self.assertEquals('retrieve-default', result.appCondition['feature'])
2973+
2974+        d = self.handleRequest(xml)
2975+        self.assertFailure(d, error.StanzaError)
2976+        d.addCallback(cb)
2977+        return d
2978+
2979+
2980+    def test_getConfiguration(self):
2981+        """
2982+        Non-overridden L{PubSubService.getConfiguration} yields unsupported
2983+        error.
2984+        """
2985+
2986+        xml = """
2987+        <iq type='get' to='pubsub.example.org'
2988+                       from='user@example.org'>
2989+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2990+            <configure/>
2991+          </pubsub>
2992+        </iq>
2993+        """
2994+
2995+        def cb(result):
2996+            self.assertEquals('feature-not-implemented', result.condition)
2997+            self.assertEquals('unsupported', result.appCondition.name)
2998+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2999+            self.assertEquals('config-node', result.appCondition['feature'])
3000+
3001+        d = self.handleRequest(xml)
3002+        self.assertFailure(d, error.StanzaError)
3003+        d.addCallback(cb)
3004+        return d
3005+
3006+
3007+    def test_setConfiguration(self):
3008+        """
3009+        Non-overridden L{PubSubService.setConfiguration} yields unsupported
3010+        error.
3011+        """
3012+
3013+        xml = """
3014+        <iq type='set' to='pubsub.example.org'
3015+                       from='user@example.org'>
3016+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
3017+            <configure node='test'>
3018+              <x xmlns='jabber:x:data' type='submit'>
3019+                <field var='FORM_TYPE' type='hidden'>
3020+                  <value>http://jabber.org/protocol/pubsub#node_config</value>
3021+                </field>
3022+                <field var='pubsub#deliver_payloads'><value>0</value></field>
3023+                <field var='pubsub#persist_items'><value>1</value></field>
3024+              </x>
3025+            </configure>
3026+          </pubsub>
3027+        </iq>
3028+        """
3029+
3030+        def cb(result):
3031+            self.assertEquals('feature-not-implemented', result.condition)
3032+            self.assertEquals('unsupported', result.appCondition.name)
3033+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
3034+            self.assertEquals('config-node', result.appCondition['feature'])
3035+
3036+        d = self.handleRequest(xml)
3037+        self.assertFailure(d, error.StanzaError)
3038+        d.addCallback(cb)
3039+        return d
3040+
3041+
3042+    def test_items(self):
3043+        """
3044+        Non-overridden L{PubSubService.items} yields unsupported error.
3045+        """
3046+        xml = """
3047+        <iq type='get' to='pubsub.example.org'
3048+                       from='user@example.org'>
3049+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
3050+            <items node='test'/>
3051+          </pubsub>
3052+        </iq>
3053+        """
3054+
3055+        def cb(result):
3056+            self.assertEquals('feature-not-implemented', result.condition)
3057+            self.assertEquals('unsupported', result.appCondition.name)
3058+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
3059+            self.assertEquals('retrieve-items', result.appCondition['feature'])
3060+
3061+        d = self.handleRequest(xml)
3062+        self.assertFailure(d, error.StanzaError)
3063+        d.addCallback(cb)
3064+        return d
3065+
3066+
3067+    def test_retract(self):
3068+        """
3069+        Non-overridden L{PubSubService.retract} yields unsupported error.
3070+        """
3071+        xml = """
3072+        <iq type='set' to='pubsub.example.org'
3073+                       from='user@example.org'>
3074+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
3075+            <retract node='test'>
3076+              <item id='item1'/>
3077+              <item id='item2'/>
3078+            </retract>
3079+          </pubsub>
3080+        </iq>
3081+        """
3082+
3083+        def cb(result):
3084+            self.assertEquals('feature-not-implemented', result.condition)
3085+            self.assertEquals('unsupported', result.appCondition.name)
3086+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
3087+            self.assertEquals('retract-items', result.appCondition['feature'])
3088+
3089+        d = self.handleRequest(xml)
3090+        self.assertFailure(d, error.StanzaError)
3091+        d.addCallback(cb)
3092+        return d
3093+
3094+
3095+    def test_purge(self):
3096+        """
3097+        Non-overridden L{PubSubService.purge} yields unsupported error.
3098+        """
3099+        xml = """
3100+        <iq type='set' to='pubsub.example.org'
3101+                       from='user@example.org'>
3102+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
3103+            <purge node='test'/>
3104+          </pubsub>
3105+        </iq>
3106+        """
3107+
3108+        def cb(result):
3109+            self.assertEquals('feature-not-implemented', result.condition)
3110+            self.assertEquals('unsupported', result.appCondition.name)
3111+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
3112+            self.assertEquals('purge-nodes', result.appCondition['feature'])
3113+
3114+        d = self.handleRequest(xml)
3115+        self.assertFailure(d, error.StanzaError)
3116+        d.addCallback(cb)
3117+        return d
3118+
3119+
3120+    def test_delete(self):
3121+        """
3122+        Non-overridden L{PubSubService.delete} yields unsupported error.
3123+        """
3124+        xml = """
3125+        <iq type='set' to='pubsub.example.org'
3126+                       from='user@example.org'>
3127+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
3128+            <delete node='test'/>
3129+          </pubsub>
3130+        </iq>
3131+        """
3132+
3133+        def cb(result):
3134+            self.assertEquals('feature-not-implemented', result.condition)
3135+            self.assertEquals('unsupported', result.appCondition.name)
3136+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
3137+            self.assertEquals('delete-nodes', result.appCondition['feature'])
3138+
3139+        d = self.handleRequest(xml)
3140+        self.assertFailure(d, error.StanzaError)
3141+        d.addCallback(cb)
3142+        return d
Note: See TracBrowser for help on using the repository browser.