source: ralphm-patches/pubsub_request.patch @ 1:7fc86463b39f

Last change on this file since 1:7fc86463b39f was 1:7fc86463b39f, checked in by Ralph Meijer <ralphm@…>, 13 years ago

Add patches.

File size: 76.2 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
13diff -r fafe0148a94c wokkel/iwokkel.py
14--- a/wokkel/iwokkel.py Wed Feb 25 09:21:33 2009 +0100
15+++ b/wokkel/iwokkel.py Thu Feb 26 09:29:59 2009 +0100
16@@ -278,6 +278,7 @@
17                              C{list} of L{domish.Element})
18         """
19 
20+
21     def notifyDelete(service, nodeIdentifier, subscribers,
22                      redirectURI=None):
23         """
24@@ -295,77 +296,60 @@
25         @type redirectURI: C{str}
26         """
27 
28-    def publish(requestor, service, nodeIdentifier, items):
29+
30+    def publish(request):
31         """
32         Called when a publish request has been received.
33 
34-        @param requestor: The entity the request originated from.
35-        @type requestor: L{jid.JID}
36-        @param service: The entity the request was addressed to.
37-        @type service: L{jid.JID}
38-        @param nodeIdentifier: The identifier of the node to publish to.
39-        @type nodeIdentifier: C{unicode}
40-        @param items: The items to be published as L{domish} elements.
41-        @type items: C{list} of C{domish.Element}
42+        @param request: The publish-subscribe request.
43+        @type request: L{wokkel.pubsub.PubSubRequest}
44         @return: deferred that fires on success.
45         @rtype: L{defer.Deferred}
46         """
47 
48-    def subscribe(requestor, service, nodeIdentifier, subscriber):
49+
50+    def subscribe(request):
51         """
52         Called when a subscribe request has been received.
53 
54-        @param requestor: The entity the request originated from.
55-        @type requestor: L{jid.JID}
56-        @param service: The entity the request was addressed to.
57-        @type service: L{jid.JID}
58-        @param nodeIdentifier: The identifier of the node to subscribe to.
59-        @type nodeIdentifier: C{unicode}
60-        @param subscriber: The entity to be subscribed.
61-        @type subscriber: L{jid.JID}
62+        @param request: The publish-subscribe request.
63+        @type request: L{wokkel.pubsub.PubSubRequest}
64         @return: A deferred that fires with a
65                  L{Subscription<wokkel.pubsub.Subscription>}.
66         @rtype: L{defer.Deferred}
67         """
68 
69-    def unsubscribe(requestor, service, nodeIdentifier, subscriber):
70+
71+    def unsubscribe(request):
72         """
73         Called when a subscribe request has been received.
74 
75-        @param requestor: The entity the request originated from.
76-        @type requestor: L{jid.JID}
77-        @param service: The entity the request was addressed to.
78-        @type service: L{jid.JID}
79-        @param nodeIdentifier: The identifier of the node to unsubscribe from.
80-        @type nodeIdentifier: C{unicode}
81-        @param subscriber: The entity to be unsubscribed.
82-        @type subscriber: L{jid.JID}
83+        @param request: The publish-subscribe request.
84+        @type request: L{wokkel.pubsub.PubSubRequest}
85         @return: A deferred that fires with C{None} when unsubscription has
86                  succeeded.
87         @rtype: L{defer.Deferred}
88         """
89 
90-    def subscriptions(requestor, service):
91+
92+    def subscriptions(request):
93         """
94         Called when a subscriptions retrieval request has been received.
95 
96-        @param requestor: The entity the request originated from.
97-        @type requestor: L{jid.JID}
98-        @param service: The entity the request was addressed to.
99-        @type service: L{jid.JID}
100+        @param request: The publish-subscribe request.
101+        @type request: L{wokkel.pubsub.PubSubRequest}
102         @return: A deferred that fires with a C{list} of subscriptions as
103                  L{Subscription<wokkel.pubsub.Subscription>}.
104         @rtype: L{defer.Deferred}
105         """
106 
107-    def affiliations(requestor, service):
108+
109+    def affiliations(request):
110         """
111         Called when a affiliations retrieval request has been received.
112 
113-        @param requestor: The entity the request originated from.
114-        @type requestor: L{jid.JID}
115-        @param service: The entity the request was addressed to.
116-        @type service: L{jid.JID}
117+        @param request: The publish-subscribe request.
118+        @type request: L{wokkel.pubsub.PubSubRequest}
119         @return: A deferred that fires with a C{list} of affiliations as
120                  C{tuple}s of (node identifier as C{unicode}, affiliation state
121                  as C{str}). The affiliation can be C{'owner'}, C{'publisher'},
122@@ -373,23 +357,18 @@
123         @rtype: L{defer.Deferred}
124         """
125 
126-    def create(requestor, service, nodeIdentifier):
127+
128+    def create(request):
129         """
130         Called when a node creation request has been received.
131 
132-        @param requestor: The entity the request originated from.
133-        @type requestor: L{jid.JID}
134-        @param service: The entity the request was addressed to.
135-        @type service: L{jid.JID}
136-        @param nodeIdentifier: The suggestion for the identifier of the node to
137-                               be created. If the request did not include a
138-                               suggestion for the node identifier, the value
139-                               is C{None}.
140-        @type nodeIdentifier: C{unicode} or C{NoneType}
141+        @param request: The publish-subscribe request.
142+        @type request: L{wokkel.pubsub.PubSubRequest}
143         @return: A deferred that fires with a C{unicode} that represents
144                  the identifier of the new node.
145         @rtype: L{defer.Deferred}
146         """
147+
148 
149     def getConfigurationOptions():
150         """
151@@ -426,17 +405,13 @@
152         @rtype: C{dict}.
153         """
154 
155-    def getDefaultConfiguration(requestor, service, nodeType):
156+
157+    def getDefaultConfiguration(request):
158         """
159         Called when a default node configuration request has been received.
160 
161-        @param requestor: The entity the request originated from.
162-        @type requestor: L{jid.JID}
163-        @param service: The entity the request was addressed to.
164-        @type service: L{jid.JID}
165-        @param nodeType: The type of node for which the configuration is
166-                         retrieved, C{'leaf'} or C{'collection'}.
167-        @type nodeType: C{str}
168+        @param request: The publish-subscribe request.
169+        @type request: L{wokkel.pubsub.PubSubRequest}
170         @return: A deferred that fires with a C{dict} representing the default
171                  node configuration. Keys are C{str}s that represent the
172                  field name. Values can be of types C{unicode}, C{int} or
173@@ -444,85 +419,74 @@
174         @rtype: L{defer.Deferred}
175         """
176 
177-    def getConfiguration(requestor, service, nodeIdentifier):
178+
179+    def getConfiguration(request):
180         """
181         Called when a node configuration retrieval request has been received.
182 
183-        @param requestor: The entity the request originated from.
184-        @type requestor: L{jid.JID}
185-        @param service: The entity the request was addressed to.
186-        @type service: L{jid.JID}
187-        @param nodeIdentifier: The identifier of the node to retrieve the
188-                               configuration from.
189-        @type nodeIdentifier: C{unicode}
190+        @param request: The publish-subscribe request.
191+        @type request: L{wokkel.pubsub.PubSubRequest}
192         @return: A deferred that fires with a C{dict} representing the node
193                  configuration. Keys are C{str}s that represent the field name.
194                  Values can be of types C{unicode}, C{int} or C{bool}.
195         @rtype: L{defer.Deferred}
196         """
197 
198-    def setConfiguration(requestor, service, nodeIdentifier, options):
199+
200+    def setConfiguration(request):
201         """
202         Called when a node configuration change request has been received.
203 
204-        @param requestor: The entity the request originated from.
205-        @type requestor: L{jid.JID}
206-        @param service: The entity the request was addressed to.
207-        @type service: L{jid.JID}
208-        @param nodeIdentifier: The identifier of the node to change the
209-                               configuration of.
210-        @type nodeIdentifier: C{unicode}
211+        @param request: The publish-subscribe request.
212+        @type request: L{wokkel.pubsub.PubSubRequest}
213         @return: A deferred that fires with C{None} when the node's
214                  configuration has been changed.
215         @rtype: L{defer.Deferred}
216         """
217 
218-    def items(requestor, service, nodeIdentifier, maxItems, itemIdentifiers):
219+
220+    def items(request):
221         """
222         Called when a items retrieval request has been received.
223 
224-        @param requestor: The entity the request originated from.
225-        @type requestor: L{jid.JID}
226-        @param service: The entity the request was addressed to.
227-        @type service: L{jid.JID}
228-        @param nodeIdentifier: The identifier of the node to retrieve items
229-                               from.
230-        @type nodeIdentifier: C{unicode}
231+        @param request: The publish-subscribe request.
232+        @type request: L{wokkel.pubsub.PubSubRequest}
233+        @return: A deferred that fires with a C{list} of L{pubsub.Item}.
234+        @rtype: L{defer.Deferred}
235         """
236 
237-    def retract(requestor, service, nodeIdentifier, itemIdentifiers):
238+
239+    def retract(request):
240         """
241         Called when a item retraction request has been received.
242 
243-        @param requestor: The entity the request originated from.
244-        @type requestor: L{jid.JID}
245-        @param service: The entity the request was addressed to.
246-        @type service: L{jid.JID}
247-        @param nodeIdentifier: The identifier of the node to retract items
248-                               from.
249-        @type nodeIdentifier: C{unicode}
250+        @param request: The publish-subscribe request.
251+        @type request: L{wokkel.pubsub.PubSubRequest}
252+        @return: A deferred that fires with C{None} when the given items have
253+                 been retracted.
254+        @rtype: L{defer.Deferred}
255         """
256 
257-    def purge(requestor, service, nodeIdentifier):
258+
259+    def purge(request):
260         """
261         Called when a node purge request has been received.
262 
263-        @param requestor: The entity the request originated from.
264-        @type requestor: L{jid.JID}
265-        @param service: The entity the request was addressed to.
266-        @type service: L{jid.JID}
267-        @param nodeIdentifier: The identifier of the node to be purged.
268-        @type nodeIdentifier: C{unicode}
269+        @param request: The publish-subscribe request.
270+        @type request: L{wokkel.pubsub.PubSubRequest}
271+        @return: A deferred that fires with C{None} when the node has been
272+                 purged.
273+        @rtype: L{defer.Deferred}
274         """
275 
276-    def delete(requestor, service, nodeIdentifier):
277+
278+    def delete(request):
279         """
280         Called when a node deletion request has been received.
281 
282-        @param requestor: The entity the request originated from.
283-        @type requestor: L{jid.JID}
284-        @param service: The entity the request was addressed to.
285-        @type service: L{jid.JID}
286-        @param nodeIdentifier: The identifier of the node to be delete.
287-        @type nodeIdentifier: C{unicode}
288+        @param request: The publish-subscribe request.
289+        @type request: L{wokkel.pubsub.PubSubRequest}
290+        @return: A deferred that fires with C{None} when the node has been
291+                 deleted.
292+        @rtype: L{defer.Deferred}
293         """
294diff -r fafe0148a94c wokkel/pubsub.py
295--- a/wokkel/pubsub.py  Wed Feb 25 09:21:33 2009 +0100
296+++ b/wokkel/pubsub.py  Thu Feb 26 09:29:59 2009 +0100
297@@ -32,42 +32,10 @@
298 NS_PUBSUB_NODE_CONFIG = NS_PUBSUB + "#node_config"
299 NS_PUBSUB_META_DATA = NS_PUBSUB + "#meta-data"
300 
301-# In publish-subscribe namespace XPath query selector.
302-IN_NS_PUBSUB = '[@xmlns="' + NS_PUBSUB + '"]'
303-IN_NS_PUBSUB_OWNER = '[@xmlns="' + NS_PUBSUB_OWNER + '"]'
304-
305-# Publish-subscribe XPath queries
306-PUBSUB_ELEMENT = '/pubsub' + IN_NS_PUBSUB
307-PUBSUB_OWNER_ELEMENT = '/pubsub' + IN_NS_PUBSUB_OWNER
308-PUBSUB_GET = IQ_GET + PUBSUB_ELEMENT
309-PUBSUB_SET = IQ_SET + PUBSUB_ELEMENT
310-PUBSUB_OWNER_GET = IQ_GET + PUBSUB_OWNER_ELEMENT
311-PUBSUB_OWNER_SET = IQ_SET + PUBSUB_OWNER_ELEMENT
312-
313-# Publish-subscribe command XPath queries
314-PUBSUB_PUBLISH = PUBSUB_SET + '/publish' + IN_NS_PUBSUB
315-PUBSUB_CREATE = PUBSUB_SET + '/create' + IN_NS_PUBSUB
316-PUBSUB_SUBSCRIBE = PUBSUB_SET + '/subscribe' + IN_NS_PUBSUB
317-PUBSUB_UNSUBSCRIBE = PUBSUB_SET + '/unsubscribe' + IN_NS_PUBSUB
318-PUBSUB_OPTIONS_GET = PUBSUB_GET + '/options' + IN_NS_PUBSUB
319-PUBSUB_OPTIONS_SET = PUBSUB_SET + '/options' + IN_NS_PUBSUB
320-PUBSUB_DEFAULT = PUBSUB_OWNER_GET + '/default' + IN_NS_PUBSUB_OWNER
321-PUBSUB_CONFIGURE_GET = PUBSUB_OWNER_GET + '/configure' + IN_NS_PUBSUB_OWNER
322-PUBSUB_CONFIGURE_SET = PUBSUB_OWNER_SET + '/configure' + IN_NS_PUBSUB_OWNER
323-PUBSUB_SUBSCRIPTIONS = PUBSUB_GET + '/subscriptions' + IN_NS_PUBSUB
324-PUBSUB_AFFILIATIONS = PUBSUB_GET + '/affiliations' + IN_NS_PUBSUB
325-PUBSUB_AFFILIATIONS_GET = PUBSUB_OWNER_GET + '/affiliations' + \
326-                          IN_NS_PUBSUB_OWNER
327-PUBSUB_AFFILIATIONS_SET = PUBSUB_OWNER_SET + '/affiliations' + \
328-                          IN_NS_PUBSUB_OWNER
329-PUBSUB_SUBSCRIPTIONS_GET = PUBSUB_OWNER_GET + '/subscriptions' + \
330-                          IN_NS_PUBSUB_OWNER
331-PUBSUB_SUBSCRIPTIONS_SET = PUBSUB_OWNER_SET + '/subscriptions' + \
332-                          IN_NS_PUBSUB_OWNER
333-PUBSUB_ITEMS = PUBSUB_GET + '/items' + IN_NS_PUBSUB
334-PUBSUB_RETRACT = PUBSUB_SET + '/retract' + IN_NS_PUBSUB
335-PUBSUB_PURGE = PUBSUB_OWNER_SET + '/purge' + IN_NS_PUBSUB_OWNER
336-PUBSUB_DELETE = PUBSUB_OWNER_SET + '/delete' + IN_NS_PUBSUB_OWNER
337+# XPath to match pubsub requests
338+PUBSUB_REQUEST = '/iq[@type="get" or @type="set"]/' + \
339+                    'pubsub[@xmlns="' + NS_PUBSUB + '" or ' + \
340+                           '@xmlns="' + NS_PUBSUB_OWNER + '"]'
341 
342 class SubscriptionPending(Exception):
343     """
344@@ -466,6 +434,219 @@
345 
346 
347 
348+class Request(object):
349+    """
350+    An IQ request.
351+
352+    @ivar sender: The entity from which the request was received.
353+    @type sender: L{jid.JID}
354+    @ivar recipient: The entity to which the request was sent.
355+    @type recipient: L{jid.JID}
356+    """
357+
358+    sender = None
359+    recipient = None
360+    stanzaType = None
361+
362+    @classmethod
363+    def fromElement(Class, element):
364+        request = Class()
365+        request.parseElement(element)
366+        return request
367+
368+
369+    def parseElement(self, element):
370+        self.sender = jid.internJID(element['from'])
371+        if element.hasAttribute('from'):
372+            self.sender = jid.internJID(element['from'])
373+        if element.hasAttribute('to'):
374+            self.recipient = jid.internJID(element['to'])
375+        self.stanzaType = element.getAttribute('type')
376+
377+
378+
379+class PubSubRequest(Request):
380+    """
381+    A publish-subscribe request.
382+
383+    The set of instance variables used depends on the type of request. If
384+    a variable is not applicable or not passed in the request, its value is
385+    C{None}.
386+
387+    @ivar affiliations: Affiliations to be modified.
388+    @type affiliations: C{set}
389+    @ivar items: The items to be published, as L{domish.Element}s.
390+    @type items: C{list}
391+    @ivar itemIdentifiers: Identifiers of the items to be retrieved or
392+                           retracted.
393+    @type itemIdentifiers: C{set}
394+    @ivar maxItems: Maximum number of items to retrieve.
395+    @type maxItems: C{int}.
396+    @ivar nodeIdentifier: Identifier of the node the request is about.
397+    @type nodeIdentifier: C{unicode}
398+    @ivar nodeType: The type of node that should be created, or for which the
399+                    configuration is retrieved. C{'leaf'} or C{'collection'}.
400+    @type nodeType: C{str}
401+    @ivar options: Configurations options for nodes, subscriptions and publish
402+                   requests.
403+    @type options: L{data_form.Form}
404+    @ivar subscriber: The subscribing entity.
405+    @type subscriber: L{JID}
406+    @ivar subscriptionIdentifier: Identifier for a specific subscription.
407+    @type subscriptionIdentifier: C{unicode}
408+    @ivar subscriptions: Subscriptions to be modified, as a set of
409+                         L{Subscription}.
410+    @type subscriptions: C{set}
411+    """
412+
413+    verb = None
414+
415+    affiliations = None
416+    items = None
417+    itemIdentifiers = None
418+    maxItems = None
419+    nodeIdentifier = None
420+    nodeType = None
421+    options = None
422+    subscriber = None
423+    subscriptionIdentifier = None
424+    subscriptions = None
425+
426+
427+    _verbs = {
428+        ('set', NS_PUBSUB, 'publish'): ('publish', ['node', 'items']),
429+        ('set', NS_PUBSUB, 'subscribe'): ('subscribe', ['nodeOrEmpty', 'jid']),
430+        ('set', NS_PUBSUB, 'unsubscribe'): ('unsubscribe', ['node', 'jid']),
431+        ('get', NS_PUBSUB, 'options'): ('optionsGet', []),
432+        ('set', NS_PUBSUB, 'options'): ('optionsSet', []),
433+        ('get', NS_PUBSUB, 'subscriptions'): ('subscriptions', []),
434+        ('get', NS_PUBSUB, 'affiliations'): ('affiliations', []),
435+        ('set', NS_PUBSUB, 'create'): ('create', ['nodeOrNone']),
436+        ('get', NS_PUBSUB_OWNER, 'default'): ('default', ['default']),
437+        ('get', NS_PUBSUB_OWNER, 'configure'): ('configureGet',
438+                                                ['nodeOrEmpty']),
439+        ('set', NS_PUBSUB_OWNER, 'configure'): ('configureSet',
440+                                                ['nodeOrEmpty', 'configure']),
441+        ('get', NS_PUBSUB, 'items'): ('items', ['node', 'maxItems',
442+                                                'itemIdentifiers']),
443+        ('set', NS_PUBSUB, 'retract'): ('retract', ['node',
444+                                                    'itemIdentifiers']),
445+        ('set', NS_PUBSUB_OWNER, 'purge'): ('purge', ['node']),
446+        ('set', NS_PUBSUB_OWNER, 'delete'): ('delete', ['node']),
447+        ('get', NS_PUBSUB_OWNER, 'affiliations'): ('affiliationsGet', []),
448+        ('set', NS_PUBSUB_OWNER, 'affiliations'): ('affiliationsSet', []),
449+        ('get', NS_PUBSUB_OWNER, 'subscriptions'): ('subscriptionsGet', []),
450+        ('set', NS_PUBSUB_OWNER, 'subscriptions'): ('subscriptionsSet', []),
451+    }
452+
453+    @staticmethod
454+    def _findForm(element, formNamespace):
455+        if not element:
456+            return None
457+
458+        form = None
459+        for child in element.elements():
460+            try:
461+                form = data_form.Form.fromElement(child)
462+            except data_form.Error:
463+                continue
464+
465+            if form.formNamespace != NS_PUBSUB_NODE_CONFIG:
466+                continue
467+
468+        return form
469+
470+
471+    def _parse_node(self, verbElement):
472+        try:
473+            self.nodeIdentifier = verbElement["node"]
474+        except KeyError:
475+            raise BadRequest('nodeid-required')
476+
477+
478+    def _parse_nodeOrEmpty(self, verbElement):
479+        self.nodeIdentifier = verbElement.getAttribute("node", '')
480+
481+
482+    def _parse_nodeOrNone(self, verbElement):
483+        self.nodeIdentifier = verbElement.getAttribute("node")
484+
485+
486+    def _parse_items(self, verbElement):
487+        self.items = []
488+        for element in verbElement.elements():
489+            if element.uri == NS_PUBSUB and element.name == 'item':
490+                self.items.append(element)
491+
492+
493+    def _parse_jid(self, verbElement):
494+        try:
495+            self.subscriber = jid.internJID(verbElement["jid"])
496+        except KeyError:
497+            raise BadRequest('jid-required')
498+
499+
500+    def _parse_default(self, verbElement):
501+        form = PubSubRequest._findForm(verbElement, NS_PUBSUB_NODE_CONFIG)
502+        if form and form.formType == 'submit':
503+            values = form.getValues()
504+            self.nodeType = values.get('pubsub#node_type', 'leaf')
505+        else:
506+            self.nodeType = 'leaf'
507+
508+
509+    def _parse_configure(self, verbElement):
510+        form = PubSubRequest._findForm(verbElement, NS_PUBSUB_NODE_CONFIG)
511+        if form:
512+            if form.formType == 'submit':
513+                self.options = form.getValues()
514+            elif form.formType == 'cancel':
515+                self.options = {}
516+            else:
517+                raise BadRequest("Unexpected form type %r" % form.formType)
518+        else:
519+            raise BadRequest("Missing configuration form")
520+
521+
522+
523+    def _parse_itemIdentifiers(self, verbElement):
524+        self.itemIdentifiers = []
525+        for element in verbElement.elements():
526+            if element.uri == NS_PUBSUB and element.name == 'item':
527+                try:
528+                    self.itemIdentifiers.append(element["id"])
529+                except KeyError:
530+                    raise BadRequest()
531+
532+
533+    def _parse_maxItems(self, verbElement):
534+        value = verbElement.getAttribute('max_items')
535+
536+        if value:
537+            try:
538+                self.maxItems = int(value)
539+            except ValueError:
540+                raise BadRequest(text="Field max_items requires a positive " +
541+                                      "integer value")
542+
543+
544+    def parseElement(self, element):
545+        Request.parseElement(self, element)
546+
547+        for child in element.pubsub.elements():
548+            key = (self.stanzaType, child.uri, child.name)
549+            self.verb, parsers = PubSubRequest._verbs.get(key)
550+            if self.verb:
551+                break
552+
553+        if not self.verb:
554+            raise NotImplementedError()
555+
556+        for parser in parsers:
557+            getattr(self, '_parse_%s' % parser)(child)
558+
559+
560+
561 class PubSubService(XMPPHandler, IQHandlerMixin):
562     """
563     Protocol implementation for a XMPP Publish Subscribe Service.
564@@ -497,27 +678,7 @@
565     implements(IPubSubService)
566 
567     iqHandlers = {
568-            PUBSUB_PUBLISH: '_onPublish',
569-            PUBSUB_CREATE: '_onCreate',
570-            PUBSUB_SUBSCRIBE: '_onSubscribe',
571-            PUBSUB_OPTIONS_GET: '_onOptionsGet',
572-            PUBSUB_OPTIONS_SET: '_onOptionsSet',
573-            PUBSUB_AFFILIATIONS: '_onAffiliations',
574-            PUBSUB_ITEMS: '_onItems',
575-            PUBSUB_RETRACT: '_onRetract',
576-            PUBSUB_SUBSCRIPTIONS: '_onSubscriptions',
577-            PUBSUB_UNSUBSCRIBE: '_onUnsubscribe',
578-
579-            PUBSUB_AFFILIATIONS_GET: '_onAffiliationsGet',
580-            PUBSUB_AFFILIATIONS_SET: '_onAffiliationsSet',
581-            PUBSUB_CONFIGURE_GET: '_onConfigureGet',
582-            PUBSUB_CONFIGURE_SET: '_onConfigureSet',
583-            PUBSUB_DEFAULT: '_onDefault',
584-            PUBSUB_PURGE: '_onPurge',
585-            PUBSUB_DELETE: '_onDelete',
586-            PUBSUB_SUBSCRIPTIONS_GET: '_onSubscriptionsGet',
587-            PUBSUB_SUBSCRIPTIONS_SET: '_onSubscriptionsSet',
588-
589+            '/*': '_onPubSubRequest',
590             }
591 
592 
593@@ -530,10 +691,7 @@
594 
595 
596     def connectionMade(self):
597-        self.xmlstream.addObserver(PUBSUB_GET, self.handleRequest)
598-        self.xmlstream.addObserver(PUBSUB_SET, self.handleRequest)
599-        self.xmlstream.addObserver(PUBSUB_OWNER_GET, self.handleRequest)
600-        self.xmlstream.addObserver(PUBSUB_OWNER_SET, self.handleRequest)
601+        self.xmlstream.addObserver(PUBSUB_REQUEST, self.handleRequest)
602 
603 
604     def getDiscoInfo(self, requestor, target, nodeIdentifier):
605@@ -585,92 +743,17 @@
606         return d
607 
608 
609-    def _findForm(self, element, formNamespace):
610-        if not element:
611-            return None
612+    def _onPubSubRequest(self, iq):
613+        request = PubSubRequest.fromElement(iq)
614+        handler = getattr(self, '_on_%s' % (request.verb))
615+        return handler(request)
616 
617-        form = None
618-        for child in element.elements():
619-            try:
620-                form = data_form.Form.fromElement(child)
621-            except data_form.Error:
622-                continue
623 
624-            if form.formNamespace != NS_PUBSUB_NODE_CONFIG:
625-                continue
626+    def _on_publish(self, request):
627+        return self.publish(request)
628 
629-        return form
630 
631-
632-    def _getParameter_node(self, commandElement):
633-        try:
634-            return commandElement["node"]
635-        except KeyError:
636-            raise BadRequest('nodeid-required')
637-
638-
639-    def _getParameter_nodeOrEmpty(self, commandElement):
640-        return commandElement.getAttribute("node", '')
641-
642-
643-    def _getParameter_jid(self, commandElement):
644-        try:
645-            return jid.internJID(commandElement["jid"])
646-        except KeyError:
647-            raise BadRequest('jid-required')
648-
649-
650-    def _getParameter_max_items(self, commandElement):
651-        value = commandElement.getAttribute('max_items')
652-
653-        if value:
654-            try:
655-                return int(value)
656-            except ValueError:
657-                raise BadRequest(text="Field max_items requires a positive " +
658-                                      "integer value")
659-        else:
660-            return None
661-
662-
663-    def _getParameters(self, iq, *names):
664-        requestor = jid.internJID(iq["from"]).userhostJID()
665-        service = jid.internJID(iq["to"])
666-
667-        params = [requestor, service]
668-
669-        if names:
670-            command = names[0]
671-            commandElement = getattr(iq.pubsub, command)
672-            if not commandElement:
673-                raise Exception("Could not find command element %r" % command)
674-
675-        for name in names[1:]:
676-            try:
677-                getter = getattr(self, '_getParameter_' + name)
678-            except KeyError:
679-                raise Exception("No parameter getter for this name")
680-
681-            params.append(getter(commandElement))
682-
683-        return params
684-
685-
686-    def _onPublish(self, iq):
687-        requestor, service, nodeIdentifier = self._getParameters(
688-                iq, 'publish', 'node')
689-
690-        items = []
691-        for element in iq.pubsub.publish.elements():
692-            if element.uri == NS_PUBSUB and element.name == 'item':
693-                items.append(element)
694-
695-        return self.publish(requestor, service, nodeIdentifier, items)
696-
697-
698-    def _onSubscribe(self, iq):
699-        requestor, service, nodeIdentifier, subscriber = self._getParameters(
700-                iq, 'subscribe', 'nodeOrEmpty', 'jid')
701+    def _on_subscribe(self, request):
702 
703         def toResponse(result):
704             response = domish.Element((NS_PUBSUB, "pubsub"))
705@@ -681,28 +764,24 @@
706             subscription["subscription"] = result.state
707             return response
708 
709-        d = self.subscribe(requestor, service, nodeIdentifier, subscriber)
710+        d = self.subscribe(request)
711         d.addCallback(toResponse)
712         return d
713 
714 
715-    def _onUnsubscribe(self, iq):
716-        requestor, service, nodeIdentifier, subscriber = self._getParameters(
717-                iq, 'unsubscribe', 'nodeOrEmpty', 'jid')
718+    def _on_unsubscribe(self, request):
719+        return self.unsubscribe(request)
720 
721-        return self.unsubscribe(requestor, service, nodeIdentifier, subscriber)
722 
723-
724-    def _onOptionsGet(self, iq):
725+    def _on_optionsGet(self, request):
726         raise Unsupported('subscription-options')
727 
728 
729-    def _onOptionsSet(self, iq):
730+    def _on_optionsSet(self, request):
731         raise Unsupported('subscription-options')
732 
733 
734-    def _onSubscriptions(self, iq):
735-        requestor, service = self._getParameters(iq)
736+    def _on_subscriptions(self, request):
737 
738         def toResponse(result):
739             response = domish.Element((NS_PUBSUB, 'pubsub'))
740@@ -714,13 +793,12 @@
741                 item['subscription'] = subscription.state
742             return response
743 
744-        d = self.subscriptions(requestor, service)
745+        d = self.subscriptions(request)
746         d.addCallback(toResponse)
747         return d
748 
749 
750-    def _onAffiliations(self, iq):
751-        requestor, service = self._getParameters(iq)
752+    def _on_affiliations(self, request):
753 
754         def toResponse(result):
755             response = domish.Element((NS_PUBSUB, 'pubsub'))
756@@ -733,17 +811,15 @@
757 
758             return response
759 
760-        d = self.affiliations(requestor, service)
761+        d = self.affiliations(request)
762         d.addCallback(toResponse)
763         return d
764 
765 
766-    def _onCreate(self, iq):
767-        requestor, service = self._getParameters(iq)
768-        nodeIdentifier = iq.pubsub.create.getAttribute("node")
769+    def _on_create(self, request):
770 
771         def toResponse(result):
772-            if not nodeIdentifier or nodeIdentifier != result:
773+            if not request.nodeIdentifier or request.nodeIdentifier != result:
774                 response = domish.Element((NS_PUBSUB, 'pubsub'))
775                 create = response.addElement('create')
776                 create['node'] = result
777@@ -751,7 +827,7 @@
778             else:
779                 return None
780 
781-        d = self.create(requestor, service, nodeIdentifier)
782+        d = self.create(request)
783         d.addCallback(toResponse)
784         return d
785 
786@@ -771,6 +847,7 @@
787             fields.append(data_form.Field.fromDict(option))
788         return fields
789 
790+
791     def _formFromConfiguration(self, values):
792         options = self.getConfigurationOptions()
793         fields = self._makeFields(options, values)
794@@ -779,6 +856,7 @@
795                               fields=fields)
796 
797         return form
798+
799 
800     def _checkConfiguration(self, values):
801         options = self.getConfigurationOptions()
802@@ -805,8 +883,7 @@
803         return processedValues
804 
805 
806-    def _onDefault(self, iq):
807-        requestor, service = self._getParameters(iq)
808+    def _on_default(self, request):
809 
810         def toResponse(options):
811             response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
812@@ -814,127 +891,82 @@
813             default.addChild(self._formFromConfiguration(options).toElement())
814             return response
815 
816-        form = self._findForm(iq.pubsub.config, NS_PUBSUB_NODE_CONFIG)
817-        values = form and form.formType == 'result' and form.getValues() or {}
818-        nodeType = values.get('pubsub#node_type', 'leaf')
819-
820-        if nodeType not in ('leaf', 'collections'):
821+        if request.nodeType not in ('leaf', 'collection'):
822             return defer.fail(error.StanzaError('not-acceptable'))
823 
824-        d = self.getDefaultConfiguration(requestor, service, nodeType)
825+        d = self.getDefaultConfiguration(request)
826         d.addCallback(toResponse)
827         return d
828 
829 
830-    def _onConfigureGet(self, iq):
831-        requestor, service, nodeIdentifier = self._getParameters(
832-                iq, 'configure', 'nodeOrEmpty')
833-
834+    def _on_configureGet(self, request):
835         def toResponse(options):
836             response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
837             configure = response.addElement("configure")
838-            configure.addChild(self._formFromConfiguration(options).toElement())
839+            form = self._formFromConfiguration(options)
840+            configure.addChild(form.toElement())
841 
842-            if nodeIdentifier:
843-                configure["node"] = nodeIdentifier
844+            if request.nodeIdentifier:
845+                configure["node"] = request.nodeIdentifier
846 
847             return response
848 
849-        d = self.getConfiguration(requestor, service, nodeIdentifier)
850+        d = self.getConfiguration(request)
851         d.addCallback(toResponse)
852         return d
853 
854 
855-    def _onConfigureSet(self, iq):
856-        requestor, service, nodeIdentifier = self._getParameters(
857-                iq, 'configure', 'nodeOrEmpty')
858+    def _on_configureSet(self, request):
859+        if request.options:
860+            request.options = self._checkConfiguration(request.options)
861+            return self.setConfiguration(request)
862+        else:
863+            return None
864 
865-        # Search configuration form with correct FORM_TYPE and process it
866 
867-        form = self._findForm(iq.pubsub.configure, NS_PUBSUB_NODE_CONFIG)
868 
869-        if form:
870-            if form.formType == 'submit':
871-                options = self._checkConfiguration(form.getValues())
872-
873-                return self.setConfiguration(requestor, service,
874-                                             nodeIdentifier, options)
875-            elif form.formType == 'cancel':
876-                return None
877-
878-        raise BadRequest()
879-
880-
881-    def _onItems(self, iq):
882-        requestor, service, nodeIdentifier, maxItems = self._getParameters(
883-                iq, 'items', 'nodeOrEmpty', 'max_items')
884-
885-        itemIdentifiers = []
886-        for child in iq.pubsub.items.elements():
887-            if child.name == 'item' and child.uri == NS_PUBSUB:
888-                try:
889-                    itemIdentifiers.append(child["id"])
890-                except KeyError:
891-                    raise BadRequest()
892+    def _on_items(self, request):
893 
894         def toResponse(result):
895             response = domish.Element((NS_PUBSUB, 'pubsub'))
896             items = response.addElement('items')
897-            if nodeIdentifier:
898-                items["node"] = nodeIdentifier
899+            items["node"] = request.nodeIdentifier
900 
901             for item in result:
902                 items.addChild(item)
903 
904             return response
905 
906-        d = self.items(requestor, service, nodeIdentifier, maxItems,
907-                       itemIdentifiers)
908+        d = self.items(request)
909         d.addCallback(toResponse)
910         return d
911 
912 
913-    def _onRetract(self, iq):
914-        requestor, service, nodeIdentifier = self._getParameters(
915-                iq, 'retract', 'node')
916+    def _on_retract(self, request):
917+        return self.retract(request)
918 
919-        itemIdentifiers = []
920-        for child in iq.pubsub.retract.elements():
921-            if child.uri == NS_PUBSUB and child.name == 'item':
922-                try:
923-                    itemIdentifiers.append(child["id"])
924-                except KeyError:
925-                    raise BadRequest()
926 
927-        return self.retract(requestor, service, nodeIdentifier,
928-                            itemIdentifiers)
929+    def _on_purge(self, request):
930+        return self.purge(request)
931 
932 
933-    def _onPurge(self, iq):
934-        requestor, service, nodeIdentifier = self._getParameters(
935-                iq, 'purge', 'node')
936-        return self.purge(requestor, service, nodeIdentifier)
937+    def _on_delete(self, request):
938+        return self.delete(request)
939 
940 
941-    def _onDelete(self, iq):
942-        requestor, service, nodeIdentifier = self._getParameters(
943-                iq, 'delete', 'node')
944-        return self.delete(requestor, service, nodeIdentifier)
945-
946-
947-    def _onAffiliationsGet(self, iq):
948+    def _on_affiliationsGet(self, iq):
949         raise Unsupported('modify-affiliations')
950 
951 
952-    def _onAffiliationsSet(self, iq):
953+    def _on_affiliationsSet(self, iq):
954         raise Unsupported('modify-affiliations')
955 
956 
957-    def _onSubscriptionsGet(self, iq):
958+    def _on_subscriptionsGet(self, iq):
959         raise Unsupported('manage-subscriptions')
960 
961 
962-    def _onSubscriptionsSet(self, iq):
963+    def _on_subscriptionsSet(self, iq):
964         raise Unsupported('manage-subscriptions')
965 
966     # public methods
967@@ -990,27 +1022,27 @@
968         return []
969 
970 
971-    def publish(self, requestor, service, nodeIdentifier, items):
972+    def publish(self, request):
973         raise Unsupported('publish')
974 
975 
976-    def subscribe(self, requestor, service, nodeIdentifier, subscriber):
977+    def subscribe(self, request):
978         raise Unsupported('subscribe')
979 
980 
981-    def unsubscribe(self, requestor, service, nodeIdentifier, subscriber):
982+    def unsubscribe(self, request):
983         raise Unsupported('subscribe')
984 
985 
986-    def subscriptions(self, requestor, service):
987+    def subscriptions(self, request):
988         raise Unsupported('retrieve-subscriptions')
989 
990 
991-    def affiliations(self, requestor, service):
992+    def affiliations(self, request):
993         raise Unsupported('retrieve-affiliations')
994 
995 
996-    def create(self, requestor, service, nodeIdentifier):
997+    def create(self, request):
998         raise Unsupported('create-nodes')
999 
1000 
1001@@ -1018,30 +1050,29 @@
1002         return {}
1003 
1004 
1005-    def getDefaultConfiguration(self, requestor, service, nodeType):
1006+    def getDefaultConfiguration(self, request):
1007         raise Unsupported('retrieve-default')
1008 
1009 
1010-    def getConfiguration(self, requestor, service, nodeIdentifier):
1011+    def getConfiguration(self, request):
1012         raise Unsupported('config-node')
1013 
1014 
1015-    def setConfiguration(self, requestor, service, nodeIdentifier, options):
1016+    def setConfiguration(self, request):
1017         raise Unsupported('config-node')
1018 
1019 
1020-    def items(self, requestor, service, nodeIdentifier, maxItems,
1021-                    itemIdentifiers):
1022+    def items(self, request):
1023         raise Unsupported('retrieve-items')
1024 
1025 
1026-    def retract(self, requestor, service, nodeIdentifier, itemIdentifiers):
1027+    def retract(self, request):
1028         raise Unsupported('retract-items')
1029 
1030 
1031-    def purge(self, requestor, service, nodeIdentifier):
1032+    def purge(self, request):
1033         raise Unsupported('purge-nodes')
1034 
1035 
1036-    def delete(self, requestor, service, nodeIdentifier):
1037+    def delete(self, request):
1038         raise Unsupported('delete-nodes')
1039diff -r fafe0148a94c wokkel/test/test_pubsub.py
1040--- a/wokkel/test/test_pubsub.py        Wed Feb 25 09:21:33 2009 +0100
1041+++ b/wokkel/test/test_pubsub.py        Thu Feb 26 09:29:59 2009 +0100
1042@@ -507,6 +507,29 @@
1043         verify.verifyObject(iwokkel.IPubSubService, self.service)
1044 
1045 
1046+    def test_connectionMade(self):
1047+        """
1048+        Verify setup of observers in L{pubsub.connectionMade}.
1049+        """
1050+        requests = []
1051+
1052+        def handleRequest(iq):
1053+            requests.append(iq)
1054+
1055+        self.service.xmlstream = self.stub.xmlstream
1056+        self.service.handleRequest = handleRequest
1057+        self.service.connectionMade()
1058+
1059+        for namespace in (NS_PUBSUB, NS_PUBSUB_OWNER):
1060+            for stanzaType in ('get', 'set'):
1061+                iq = domish.Element((None, 'iq'))
1062+                iq['type'] = stanzaType
1063+                iq.addElement((namespace, 'pubsub'))
1064+                self.stub.xmlstream.dispatch(iq)
1065+
1066+        self.assertEqual(4, len(requests))
1067+
1068+
1069     def test_getDiscoInfo(self):
1070         """
1071         Test getDiscoInfo calls getNodeInfo and returns some minimal info.
1072@@ -561,24 +584,178 @@
1073         </iq>
1074         """
1075 
1076-        def publish(requestor, service, nodeIdentifier, items):
1077-            self.assertEqual(JID('user@example.org'), requestor)
1078-            self.assertEqual(JID('pubsub.example.org'), service)
1079-            self.assertEqual('test', nodeIdentifier)
1080-            self.assertEqual([], items)
1081+        def publish(request):
1082+            self.assertEqual(JID('user@example.org'), request.sender)
1083+            self.assertEqual(JID('pubsub.example.org'), request.recipient)
1084+            self.assertEqual('test', request.nodeIdentifier)
1085+            self.assertEqual([], request.items)
1086             return defer.succeed(None)
1087 
1088         self.service.publish = publish
1089+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1090         return self.handleRequest(xml)
1091+
1092+
1093+    def test_onPublishItems(self):
1094+        """
1095+        A publish request with items should pass the items onto C{publish}.
1096+        """
1097+
1098+        xml = """
1099+        <iq type='set' to='pubsub.example.org'
1100+                       from='user@example.org'>
1101+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1102+            <publish node='test'>
1103+              <item id="item1"/>
1104+              <item id="item2"/>
1105+            </publish>
1106+          </pubsub>
1107+        </iq>
1108+        """
1109+
1110+        def publish(request):
1111+            self.assertEqual(2, len(request.items))
1112+            self.assertEqual(u'item1', request.items[0]["id"])
1113+            self.assertEqual(u'item2', request.items[1]["id"])
1114+            return defer.succeed(None)
1115+
1116+        self.service.publish = publish
1117+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1118+        return self.handleRequest(xml)
1119+
1120+
1121+    def test_onSubscribe(self):
1122+        """
1123+        A successful subscription should return the current subscription.
1124+        """
1125+
1126+        xml = """
1127+        <iq type='set' to='pubsub.example.org'
1128+                       from='user@example.org'>
1129+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1130+            <subscribe node='test' jid='user@example.org/Home'/>
1131+          </pubsub>
1132+        </iq>
1133+        """
1134+
1135+        def subscribe(request):
1136+            self.assertEqual(JID('user@example.org'), request.sender)
1137+            self.assertEqual(JID('pubsub.example.org'), request.recipient)
1138+            self.assertEqual('test', request.nodeIdentifier)
1139+            self.assertEqual(JID('user@example.org/Home'), request.subscriber)
1140+            return defer.succeed(pubsub.Subscription(request.nodeIdentifier,
1141+                                                     request.subscriber,
1142+                                                     'subscribed'))
1143+
1144+        def cb(element):
1145+            self.assertEqual('pubsub', element.name)
1146+            self.assertEqual(NS_PUBSUB, element.uri)
1147+            subscription = element.subscription
1148+            self.assertEqual(NS_PUBSUB, subscription.uri)
1149+            self.assertEqual('test', subscription['node'])
1150+            self.assertEqual('user@example.org/Home', subscription['jid'])
1151+            self.assertEqual('subscribed', subscription['subscription'])
1152+
1153+        self.service.subscribe = subscribe
1154+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1155+        d = self.handleRequest(xml)
1156+        d.addCallback(cb)
1157+        return d
1158+
1159+
1160+    def test_onSubscribeEmptyNode(self):
1161+        """
1162+        A successful subscription on root node should return no node attribute.
1163+        """
1164+
1165+        xml = """
1166+        <iq type='set' to='pubsub.example.org'
1167+                       from='user@example.org'>
1168+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1169+            <subscribe jid='user@example.org/Home'/>
1170+          </pubsub>
1171+        </iq>
1172+        """
1173+
1174+        def subscribe(request):
1175+            self.assertEqual('', request.nodeIdentifier)
1176+            return defer.succeed(pubsub.Subscription(request.nodeIdentifier,
1177+                                                     request.subscriber,
1178+                                                     'subscribed'))
1179+
1180+        def cb(element):
1181+            self.assertFalse(element.subscription.hasAttribute('node'))
1182+
1183+        self.service.subscribe = subscribe
1184+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1185+        d = self.handleRequest(xml)
1186+        d.addCallback(cb)
1187+        return d
1188+
1189+
1190+    def test_onUnsubscribe(self):
1191+        """
1192+        A successful unsubscription should return an empty response.
1193+        """
1194+
1195+        xml = """
1196+        <iq type='set' to='pubsub.example.org'
1197+                       from='user@example.org'>
1198+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1199+            <unsubscribe node='test' jid='user@example.org/Home'/>
1200+          </pubsub>
1201+        </iq>
1202+        """
1203+
1204+        def unsubscribe(request):
1205+            self.assertEqual(JID('user@example.org'), request.sender)
1206+            self.assertEqual(JID('pubsub.example.org'), request.recipient)
1207+            self.assertEqual('test', request.nodeIdentifier)
1208+            self.assertEqual(JID('user@example.org/Home'), request.subscriber)
1209+            return defer.succeed(None)
1210+
1211+        def cb(element):
1212+            self.assertIdentical(None, element)
1213+
1214+        self.service.unsubscribe = unsubscribe
1215+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1216+        d = self.handleRequest(xml)
1217+        d.addCallback(cb)
1218+        return d
1219 
1220 
1221     def test_onOptionsGet(self):
1222         """
1223-        Subscription options are not supported.
1224+        Getting subscription options is not supported.
1225         """
1226 
1227         xml = """
1228         <iq type='get' to='pubsub.example.org'
1229+                       from='user@example.org'>
1230+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1231+            <options/>
1232+          </pubsub>
1233+        </iq>
1234+        """
1235+
1236+        def cb(result):
1237+            self.assertEquals('feature-not-implemented', result.condition)
1238+            self.assertEquals('unsupported', result.appCondition.name)
1239+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
1240+
1241+        d = self.handleRequest(xml)
1242+        self.assertFailure(d, error.StanzaError)
1243+        d.addCallback(cb)
1244+        return d
1245+
1246+
1247+    def test_onOptionsSet(self):
1248+        """
1249+        Setting subscription options is not supported.
1250+        """
1251+
1252+        xml = """
1253+        <iq type='set' to='pubsub.example.org'
1254                        from='user@example.org'>
1255           <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1256             <options/>
1257@@ -627,14 +804,149 @@
1258             self.assertEqual('subscribed', subscription['subscription'])
1259 
1260 
1261-        def subscriptions(requestor, service):
1262-            self.assertEqual(JID('user@example.org'), requestor)
1263-            self.assertEqual(JID('pubsub.example.org'), service)
1264+        def subscriptions(request):
1265+            self.assertEqual(JID('user@example.org'), request.sender)
1266+            self.assertEqual(JID('pubsub.example.org'), request.recipient)
1267             subscription = pubsub.Subscription('test', JID('user@example.org'),
1268                                                'subscribed')
1269             return defer.succeed([subscription])
1270 
1271         self.service.subscriptions = subscriptions
1272+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1273+        d = self.handleRequest(xml)
1274+        d.addCallback(cb)
1275+        return d
1276+
1277+
1278+    def test_onAffiliations(self):
1279+        """
1280+        A subscriptions request should result in
1281+        L{PubSubService.affiliations} being called and the result prepared
1282+        for the response.
1283+        """
1284+
1285+        xml = """
1286+        <iq type='get' to='pubsub.example.org'
1287+                       from='user@example.org'>
1288+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1289+            <affiliations/>
1290+          </pubsub>
1291+        </iq>
1292+        """
1293+
1294+        def cb(element):
1295+            self.assertEqual('pubsub', element.name)
1296+            self.assertEqual(NS_PUBSUB, element.uri)
1297+            self.assertEqual(NS_PUBSUB, element.affiliations.uri)
1298+            children = list(element.affiliations.elements())
1299+            self.assertEqual(1, len(children))
1300+            affiliation = children[0]
1301+            self.assertEqual('affiliation', affiliation.name)
1302+            self.assertEqual(NS_PUBSUB, affiliation.uri)
1303+            self.assertEqual('test', affiliation['node'])
1304+            self.assertEqual('owner', affiliation['affiliation'])
1305+
1306+
1307+        def affiliations(request):
1308+            self.assertEqual(JID('user@example.org'), request.sender)
1309+            self.assertEqual(JID('pubsub.example.org'), request.recipient)
1310+            affiliation = ('test', 'owner')
1311+            return defer.succeed([affiliation])
1312+
1313+        self.service.affiliations = affiliations
1314+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1315+        d = self.handleRequest(xml)
1316+        d.addCallback(cb)
1317+        return d
1318+
1319+
1320+    def test_onCreate(self):
1321+        """
1322+        Replies to create node requests don't return the created node.
1323+        """
1324+
1325+        xml = """
1326+        <iq type='set' to='pubsub.example.org'
1327+                       from='user@example.org'>
1328+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1329+            <create node='mynode'/>
1330+          </pubsub>
1331+        </iq>
1332+        """
1333+
1334+        def create(request):
1335+            self.assertEqual(JID('user@example.org'), request.sender)
1336+            self.assertEqual(JID('pubsub.example.org'), request.recipient)
1337+            self.assertEqual('mynode', request.nodeIdentifier)
1338+            return defer.succeed(request.nodeIdentifier)
1339+
1340+        def cb(element):
1341+            self.assertIdentical(None, element)
1342+
1343+        self.service.create = create
1344+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1345+        d = self.handleRequest(xml)
1346+        d.addCallback(cb)
1347+        return d
1348+
1349+
1350+    def test_onCreateChanged(self):
1351+        """
1352+        Replies to create node requests return the created node if changed.
1353+        """
1354+
1355+        xml = """
1356+        <iq type='set' to='pubsub.example.org'
1357+                       from='user@example.org'>
1358+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1359+            <create node='mynode'/>
1360+          </pubsub>
1361+        </iq>
1362+        """
1363+
1364+        def create(request):
1365+            return defer.succeed(u'myrenamednode')
1366+
1367+        def cb(element):
1368+            self.assertEqual('pubsub', element.name)
1369+            self.assertEqual(NS_PUBSUB, element.uri)
1370+            self.assertEqual(NS_PUBSUB, element.create.uri)
1371+            self.assertEqual(u'myrenamednode',
1372+                             element.create.getAttribute('node'))
1373+
1374+        self.service.create = create
1375+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1376+        d = self.handleRequest(xml)
1377+        d.addCallback(cb)
1378+        return d
1379+
1380+
1381+    def test_onCreateInstant(self):
1382+        """
1383+        Replies to create instant node requests return the created node.
1384+        """
1385+
1386+        xml = """
1387+        <iq type='set' to='pubsub.example.org'
1388+                       from='user@example.org'>
1389+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1390+            <create/>
1391+          </pubsub>
1392+        </iq>
1393+        """
1394+
1395+        def create(request):
1396+            self.assertIdentical(None, request.nodeIdentifier)
1397+            return defer.succeed(u'random')
1398+
1399+        def cb(element):
1400+            self.assertEqual('pubsub', element.name)
1401+            self.assertEqual(NS_PUBSUB, element.uri)
1402+            self.assertEqual(NS_PUBSUB, element.create.uri)
1403+            self.assertEqual(u'random', element.create.getAttribute('node'))
1404+
1405+        self.service.create = create
1406+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1407         d = self.handleRequest(xml)
1408         d.addCallback(cb)
1409         return d
1410@@ -665,10 +977,10 @@
1411                      "label": "Deliver payloads with event notifications"}
1412                 }
1413 
1414-        def getDefaultConfiguration(requestor, service, nodeType):
1415-            self.assertEqual(JID('user@example.org'), requestor)
1416-            self.assertEqual(JID('pubsub.example.org'), service)
1417-            self.assertEqual('leaf', nodeType)
1418+        def getDefaultConfiguration(request):
1419+            self.assertEqual(JID('user@example.org'), request.sender)
1420+            self.assertEqual(JID('pubsub.example.org'), request.recipient)
1421+            self.assertEqual('leaf', request.nodeType)
1422             return defer.succeed({})
1423 
1424         def cb(element):
1425@@ -682,6 +994,95 @@
1426         self.service.getDefaultConfiguration = getDefaultConfiguration
1427         verify.verifyObject(iwokkel.IPubSubService, self.service)
1428         d = self.handleRequest(xml)
1429+        d.addCallback(cb)
1430+        return d
1431+
1432+
1433+    def test_onDefaultCollection(self):
1434+        """
1435+        Responses to default requests should depend on passed node type.
1436+        """
1437+
1438+        xml = """
1439+        <iq type='get' to='pubsub.example.org'
1440+                       from='user@example.org'>
1441+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1442+            <default>
1443+              <x xmlns='jabber:x:data' type='submit'>
1444+                <field var='FORM_TYPE' type='hidden'>
1445+                  <value>http://jabber.org/protocol/pubsub#node_config</value>
1446+                </field>
1447+                <field var='pubsub#node_type'>
1448+                  <value>collection</value>
1449+                </field>
1450+              </x>
1451+            </default>
1452+
1453+          </pubsub>
1454+        </iq>
1455+        """
1456+
1457+        def getConfigurationOptions():
1458+            return {
1459+                "pubsub#deliver_payloads":
1460+                    {"type": "boolean",
1461+                     "label": "Deliver payloads with event notifications"}
1462+                }
1463+
1464+        def getDefaultConfiguration(request):
1465+            self.assertEqual('collection', request.nodeType)
1466+            return defer.succeed({})
1467+
1468+        def cb(element):
1469+            self.assertEqual('pubsub', element.name)
1470+            self.assertEqual(NS_PUBSUB_OWNER, element.uri)
1471+            self.assertEqual(NS_PUBSUB_OWNER, element.default.uri)
1472+            form = data_form.Form.fromElement(element.default.x)
1473+            self.assertEqual(NS_PUBSUB_CONFIG, form.formNamespace)
1474+
1475+        self.service.getConfigurationOptions = getConfigurationOptions
1476+        self.service.getDefaultConfiguration = getDefaultConfiguration
1477+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1478+        d = self.handleRequest(xml)
1479+        d.addCallback(cb)
1480+        return d
1481+
1482+
1483+    def test_onDefaultUnknownNodeType(self):
1484+        """
1485+        A default request should result in
1486+        L{PubSubService.getDefaultConfiguration} being called.
1487+        """
1488+
1489+        xml = """
1490+        <iq type='get' to='pubsub.example.org'
1491+                       from='user@example.org'>
1492+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1493+            <default>
1494+              <x xmlns='jabber:x:data' type='submit'>
1495+                <field var='FORM_TYPE' type='hidden'>
1496+                  <value>http://jabber.org/protocol/pubsub#node_config</value>
1497+                </field>
1498+                <field var='pubsub#node_type'>
1499+                  <value>unknown</value>
1500+                </field>
1501+              </x>
1502+            </default>
1503+
1504+          </pubsub>
1505+        </iq>
1506+        """
1507+
1508+        def getDefaultConfiguration(request):
1509+            self.fail("Unexpected call to getConfiguration")
1510+
1511+        def cb(result):
1512+            self.assertEquals('not-acceptable', result.condition)
1513+
1514+        self.service.getDefaultConfiguration = getDefaultConfiguration
1515+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1516+        d = self.handleRequest(xml)
1517+        self.assertFailure(d, error.StanzaError)
1518         d.addCallback(cb)
1519         return d
1520 
1521@@ -714,14 +1115,15 @@
1522                      "label": "Owner of the node"}
1523                 }
1524 
1525-        def getConfiguration(requestor, service, nodeIdentifier):
1526-            self.assertEqual(JID('user@example.org'), requestor)
1527-            self.assertEqual(JID('pubsub.example.org'), service)
1528-            self.assertEqual('test', nodeIdentifier)
1529+        def getConfiguration(request):
1530+            self.assertEqual(JID('user@example.org'), request.sender)
1531+            self.assertEqual(JID('pubsub.example.org'), request.recipient)
1532+            self.assertEqual('test', request.nodeIdentifier)
1533 
1534             return defer.succeed({'pubsub#deliver_payloads': '0',
1535                                   'pubsub#persist_items': '1',
1536-                                  'pubsub#owner': JID('user@example.org')})
1537+                                  'pubsub#owner': JID('user@example.org'),
1538+                                  'x-myfield': ['a', 'b']})
1539 
1540         def cb(element):
1541             self.assertEqual('pubsub', element.name)
1542@@ -749,8 +1151,12 @@
1543             field.typeCheck()
1544             self.assertEqual(JID('user@example.org'), field.value)
1545 
1546+            self.assertNotIn('x-myfield', fields)
1547+
1548+
1549         self.service.getConfigurationOptions = getConfigurationOptions
1550         self.service.getConfiguration = getConfiguration
1551+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1552         d = self.handleRequest(xml)
1553         d.addCallback(cb)
1554         return d
1555@@ -789,16 +1195,17 @@
1556                      "label": "Deliver payloads with event notifications"}
1557                 }
1558 
1559-        def setConfiguration(requestor, service, nodeIdentifier, options):
1560-            self.assertEqual(JID('user@example.org'), requestor)
1561-            self.assertEqual(JID('pubsub.example.org'), service)
1562-            self.assertEqual('test', nodeIdentifier)
1563+        def setConfiguration(request):
1564+            self.assertEqual(JID('user@example.org'), request.sender)
1565+            self.assertEqual(JID('pubsub.example.org'), request.recipient)
1566+            self.assertEqual('test', request.nodeIdentifier)
1567             self.assertEqual({'pubsub#deliver_payloads': False,
1568-                              'pubsub#persist_items': True}, options)
1569+                              'pubsub#persist_items': True}, request.options)
1570             return defer.succeed(None)
1571 
1572         self.service.getConfigurationOptions = getConfigurationOptions
1573         self.service.setConfiguration = setConfiguration
1574+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1575         return self.handleRequest(xml)
1576 
1577 
1578@@ -823,10 +1230,11 @@
1579         </iq>
1580         """
1581 
1582-        def setConfiguration(requestor, service, nodeIdentifier, options):
1583+        def setConfiguration(request):
1584             self.fail("Unexpected call to setConfiguration")
1585 
1586         self.service.setConfiguration = setConfiguration
1587+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1588         return self.handleRequest(xml)
1589 
1590 
1591@@ -862,12 +1270,45 @@
1592                      "label": "Deliver payloads with event notifications"}
1593                 }
1594 
1595-        def setConfiguration(requestor, service, nodeIdentifier, options):
1596-            self.assertEquals(['pubsub#deliver_payloads'], options.keys())
1597+        def setConfiguration(request):
1598+            self.assertEquals(['pubsub#deliver_payloads'],
1599+                              request.options.keys())
1600 
1601         self.service.getConfigurationOptions = getConfigurationOptions
1602         self.service.setConfiguration = setConfiguration
1603+        verify.verifyObject(iwokkel.IPubSubService, self.service)
1604         return self.handleRequest(xml)
1605+
1606+
1607+    def test_onConfigureSetBadFormType(self):
1608+        """
1609+        On a node configuration set request unknown fields should be ignored.
1610+        """
1611+
1612+        xml = """
1613+        <iq type='set' to='pubsub.example.org'
1614+                       from='user@example.org'>
1615+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1616+            <configure node='test'>
1617+              <x xmlns='jabber:x:data' type='result'>
1618+                <field var='FORM_TYPE' type='hidden'>
1619+                  <value>http://jabber.org/protocol/pubsub#node_config</value>
1620+                </field>
1621+                <field var='pubsub#deliver_payloads'><value>0</value></field>
1622+                <field var='x-myfield'><value>1</value></field>
1623+              </x>
1624+            </configure>
1625+          </pubsub>
1626+        </iq>
1627+        """
1628+
1629+        def cb(result):
1630+            self.assertEquals('bad-request', result.condition)
1631+
1632+        d = self.handleRequest(xml)
1633+        self.assertFailure(d, error.StanzaError)
1634+        d.addCallback(cb)
1635+        return d
1636 
1637 
1638     def test_onItems(self):
1639@@ -883,12 +1324,12 @@
1640         </iq>
1641         """
1642 
1643-        def items(requestor, service, nodeIdentifier, maxItems, items):
1644-            self.assertEqual(JID('user@example.org'), requestor)
1645-            self.assertEqual(JID('pubsub.example.org'), service)
1646-            self.assertEqual('test', nodeIdentifier)
1647-            self.assertIdentical(None, maxItems)
1648-            self.assertEqual([], items)
1649+        def items(request):
1650+            self.assertEqual(JID('user@example.org'), request.sender)
1651+            self.assertEqual(JID('pubsub.example.org'), request.recipient)
1652+            self.assertEqual('test', request.nodeIdentifier)
1653+            self.assertIdentical(None, request.maxItems)
1654+            self.assertEqual([], request.itemIdentifiers)
1655             return defer.succeed([pubsub.Item('current')])
1656 
1657         def cb(element):
1658@@ -925,11 +1366,11 @@
1659         </iq>
1660         """
1661 
1662-        def retract(requestor, service, nodeIdentifier, itemIdentifiers):
1663-            self.assertEqual(JID('user@example.org'), requestor)
1664-            self.assertEqual(JID('pubsub.example.org'), service)
1665-            self.assertEqual('test', nodeIdentifier)
1666-            self.assertEqual(['item1', 'item2'], itemIdentifiers)
1667+        def retract(request):
1668+            self.assertEqual(JID('user@example.org'), request.sender)
1669+            self.assertEqual(JID('pubsub.example.org'), request.recipient)
1670+            self.assertEqual('test', request.nodeIdentifier)
1671+            self.assertEqual(['item1', 'item2'], request.itemIdentifiers)
1672             return defer.succeed(None)
1673 
1674         self.service.retract = retract
1675@@ -951,10 +1392,10 @@
1676         </iq>
1677         """
1678 
1679-        def purge(requestor, service, nodeIdentifier):
1680-            self.assertEqual(JID('user@example.org'), requestor)
1681-            self.assertEqual(JID('pubsub.example.org'), service)
1682-            self.assertEqual('test', nodeIdentifier)
1683+        def purge(request):
1684+            self.assertEqual(JID('user@example.org'), request.sender)
1685+            self.assertEqual(JID('pubsub.example.org'), request.recipient)
1686+            self.assertEqual('test', request.nodeIdentifier)
1687             return defer.succeed(None)
1688 
1689         self.service.purge = purge
1690@@ -976,10 +1417,10 @@
1691         </iq>
1692         """
1693 
1694-        def delete(requestor, service, nodeIdentifier):
1695-            self.assertEqual(JID('user@example.org'), requestor)
1696-            self.assertEqual(JID('pubsub.example.org'), service)
1697-            self.assertEqual('test', nodeIdentifier)
1698+        def delete(request):
1699+            self.assertEqual(JID('user@example.org'), request.sender)
1700+            self.assertEqual(JID('pubsub.example.org'), request.recipient)
1701+            self.assertEqual('test', request.nodeIdentifier)
1702             return defer.succeed(None)
1703 
1704         self.service.delete = delete
1705@@ -1031,3 +1472,461 @@
1706         self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.redirect.uri)
1707         self.assertTrue(message.event.delete.redirect.hasAttribute('uri'))
1708         self.assertEqual(redirectURI, message.event.delete.redirect['uri'])
1709+
1710+
1711+    def test_onSubscriptionsGet(self):
1712+        """
1713+        Getting subscription options is not supported.
1714+        """
1715+
1716+        xml = """
1717+        <iq type='get' to='pubsub.example.org'
1718+                       from='user@example.org'>
1719+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1720+            <subscriptions/>
1721+          </pubsub>
1722+        </iq>
1723+        """
1724+
1725+        def cb(result):
1726+            self.assertEquals('feature-not-implemented', result.condition)
1727+            self.assertEquals('unsupported', result.appCondition.name)
1728+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
1729+            self.assertEquals('manage-subscriptions',
1730+                              result.appCondition['feature'])
1731+
1732+        d = self.handleRequest(xml)
1733+        self.assertFailure(d, error.StanzaError)
1734+        d.addCallback(cb)
1735+        return d
1736+
1737+
1738+    def test_onSubscriptionsSet(self):
1739+        """
1740+        Setting subscription options is not supported.
1741+        """
1742+
1743+        xml = """
1744+        <iq type='set' to='pubsub.example.org'
1745+                       from='user@example.org'>
1746+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1747+            <subscriptions/>
1748+          </pubsub>
1749+        </iq>
1750+        """
1751+
1752+        def cb(result):
1753+            self.assertEquals('feature-not-implemented', result.condition)
1754+            self.assertEquals('unsupported', result.appCondition.name)
1755+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
1756+            self.assertEquals('manage-subscriptions',
1757+                              result.appCondition['feature'])
1758+
1759+        d = self.handleRequest(xml)
1760+        self.assertFailure(d, error.StanzaError)
1761+        d.addCallback(cb)
1762+        return d
1763+
1764+
1765+    def test_onAffiliationsGet(self):
1766+        """
1767+        Getting subscription options is not supported.
1768+        """
1769+
1770+        xml = """
1771+        <iq type='get' to='pubsub.example.org'
1772+                       from='user@example.org'>
1773+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1774+            <affiliations/>
1775+          </pubsub>
1776+        </iq>
1777+        """
1778+
1779+        def cb(result):
1780+            self.assertEquals('feature-not-implemented', result.condition)
1781+            self.assertEquals('unsupported', result.appCondition.name)
1782+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
1783+            self.assertEquals('modify-affiliations',
1784+                              result.appCondition['feature'])
1785+
1786+        d = self.handleRequest(xml)
1787+        self.assertFailure(d, error.StanzaError)
1788+        d.addCallback(cb)
1789+        return d
1790+
1791+
1792+    def test_onAffiliationsSet(self):
1793+        """
1794+        Setting subscription options is not supported.
1795+        """
1796+
1797+        xml = """
1798+        <iq type='set' to='pubsub.example.org'
1799+                       from='user@example.org'>
1800+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1801+            <affiliations/>
1802+          </pubsub>
1803+        </iq>
1804+        """
1805+
1806+        def cb(result):
1807+            self.assertEquals('feature-not-implemented', result.condition)
1808+            self.assertEquals('unsupported', result.appCondition.name)
1809+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
1810+            self.assertEquals('modify-affiliations',
1811+                              result.appCondition['feature'])
1812+
1813+        d = self.handleRequest(xml)
1814+        self.assertFailure(d, error.StanzaError)
1815+        d.addCallback(cb)
1816+        return d
1817+
1818+
1819+    def test_publish(self):
1820+        """
1821+        Non-overridden L{PubSubService.publish} yields unsupported error.
1822+        """
1823+
1824+        xml = """
1825+        <iq type='set' to='pubsub.example.org'
1826+                       from='user@example.org'>
1827+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1828+            <publish node='mynode'/>
1829+          </pubsub>
1830+        </iq>
1831+        """
1832+
1833+        def cb(result):
1834+            self.assertEquals('feature-not-implemented', result.condition)
1835+            self.assertEquals('unsupported', result.appCondition.name)
1836+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
1837+            self.assertEquals('publish', result.appCondition['feature'])
1838+
1839+        d = self.handleRequest(xml)
1840+        self.assertFailure(d, error.StanzaError)
1841+        d.addCallback(cb)
1842+        return d
1843+
1844+
1845+    def test_subscribe(self):
1846+        """
1847+        Non-overridden L{PubSubService.subscribe} yields unsupported error.
1848+        """
1849+
1850+        xml = """
1851+        <iq type='set' to='pubsub.example.org'
1852+                       from='user@example.org'>
1853+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1854+            <subscribe node='test' jid='user@example.org/Home'/>
1855+          </pubsub>
1856+        </iq>
1857+        """
1858+
1859+        def cb(result):
1860+            self.assertEquals('feature-not-implemented', result.condition)
1861+            self.assertEquals('unsupported', result.appCondition.name)
1862+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
1863+            self.assertEquals('subscribe', result.appCondition['feature'])
1864+
1865+        d = self.handleRequest(xml)
1866+        self.assertFailure(d, error.StanzaError)
1867+        d.addCallback(cb)
1868+        return d
1869+
1870+
1871+    def test_unsubscribe(self):
1872+        """
1873+        Non-overridden L{PubSubService.unsubscribe} yields unsupported error.
1874+        """
1875+
1876+        xml = """
1877+        <iq type='set' to='pubsub.example.org'
1878+                       from='user@example.org'>
1879+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1880+            <unsubscribe node='test' jid='user@example.org/Home'/>
1881+          </pubsub>
1882+        </iq>
1883+        """
1884+
1885+        def cb(result):
1886+            self.assertEquals('feature-not-implemented', result.condition)
1887+            self.assertEquals('unsupported', result.appCondition.name)
1888+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
1889+            self.assertEquals('subscribe', result.appCondition['feature'])
1890+
1891+        d = self.handleRequest(xml)
1892+        self.assertFailure(d, error.StanzaError)
1893+        d.addCallback(cb)
1894+        return d
1895+
1896+
1897+    def test_subscriptions(self):
1898+        """
1899+        Non-overridden L{PubSubService.subscriptions} yields unsupported error.
1900+        """
1901+
1902+        xml = """
1903+        <iq type='get' to='pubsub.example.org'
1904+                       from='user@example.org'>
1905+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1906+            <subscriptions/>
1907+          </pubsub>
1908+        </iq>
1909+        """
1910+
1911+        def cb(result):
1912+            self.assertEquals('feature-not-implemented', result.condition)
1913+            self.assertEquals('unsupported', result.appCondition.name)
1914+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
1915+            self.assertEquals('retrieve-subscriptions',
1916+                              result.appCondition['feature'])
1917+
1918+        d = self.handleRequest(xml)
1919+        self.assertFailure(d, error.StanzaError)
1920+        d.addCallback(cb)
1921+        return d
1922+
1923+
1924+    def test_affiliations(self):
1925+        """
1926+        Non-overridden L{PubSubService.affiliations} yields unsupported error.
1927+        """
1928+
1929+        xml = """
1930+        <iq type='get' to='pubsub.example.org'
1931+                       from='user@example.org'>
1932+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1933+            <affiliations/>
1934+          </pubsub>
1935+        </iq>
1936+        """
1937+
1938+        def cb(result):
1939+            self.assertEquals('feature-not-implemented', result.condition)
1940+            self.assertEquals('unsupported', result.appCondition.name)
1941+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
1942+            self.assertEquals('retrieve-affiliations',
1943+                              result.appCondition['feature'])
1944+
1945+        d = self.handleRequest(xml)
1946+        self.assertFailure(d, error.StanzaError)
1947+        d.addCallback(cb)
1948+        return d
1949+
1950+
1951+    def test_create(self):
1952+        """
1953+        Non-overridden L{PubSubService.create} yields unsupported error.
1954+        """
1955+
1956+        xml = """
1957+        <iq type='set' to='pubsub.example.org'
1958+                       from='user@example.org'>
1959+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
1960+            <create node='mynode'/>
1961+          </pubsub>
1962+        </iq>
1963+        """
1964+
1965+        def cb(result):
1966+            self.assertEquals('feature-not-implemented', result.condition)
1967+            self.assertEquals('unsupported', result.appCondition.name)
1968+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
1969+            self.assertEquals('create-nodes', result.appCondition['feature'])
1970+
1971+        d = self.handleRequest(xml)
1972+        self.assertFailure(d, error.StanzaError)
1973+        d.addCallback(cb)
1974+        return d
1975+
1976+
1977+    def test_getDefaultConfiguration(self):
1978+        """
1979+        Non-overridden L{PubSubService.getDefaultConfiguration} yields
1980+        unsupported error.
1981+        """
1982+
1983+        xml = """
1984+        <iq type='get' to='pubsub.example.org'
1985+                       from='user@example.org'>
1986+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
1987+            <default/>
1988+          </pubsub>
1989+        </iq>
1990+        """
1991+
1992+        def cb(result):
1993+            self.assertEquals('feature-not-implemented', result.condition)
1994+            self.assertEquals('unsupported', result.appCondition.name)
1995+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
1996+            self.assertEquals('retrieve-default', result.appCondition['feature'])
1997+
1998+        d = self.handleRequest(xml)
1999+        self.assertFailure(d, error.StanzaError)
2000+        d.addCallback(cb)
2001+        return d
2002+
2003+
2004+    def test_getConfiguration(self):
2005+        """
2006+        Non-overridden L{PubSubService.getConfiguration} yields unsupported
2007+        error.
2008+        """
2009+
2010+        xml = """
2011+        <iq type='get' to='pubsub.example.org'
2012+                       from='user@example.org'>
2013+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2014+            <configure/>
2015+          </pubsub>
2016+        </iq>
2017+        """
2018+
2019+        def cb(result):
2020+            self.assertEquals('feature-not-implemented', result.condition)
2021+            self.assertEquals('unsupported', result.appCondition.name)
2022+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2023+            self.assertEquals('config-node', result.appCondition['feature'])
2024+
2025+        d = self.handleRequest(xml)
2026+        self.assertFailure(d, error.StanzaError)
2027+        d.addCallback(cb)
2028+        return d
2029+
2030+
2031+    def test_setConfiguration(self):
2032+        """
2033+        Non-overridden L{PubSubService.setConfiguration} yields unsupported
2034+        error.
2035+        """
2036+
2037+        xml = """
2038+        <iq type='set' to='pubsub.example.org'
2039+                       from='user@example.org'>
2040+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2041+            <configure node='test'>
2042+              <x xmlns='jabber:x:data' type='submit'>
2043+                <field var='FORM_TYPE' type='hidden'>
2044+                  <value>http://jabber.org/protocol/pubsub#node_config</value>
2045+                </field>
2046+                <field var='pubsub#deliver_payloads'><value>0</value></field>
2047+                <field var='pubsub#persist_items'><value>1</value></field>
2048+              </x>
2049+            </configure>
2050+          </pubsub>
2051+        </iq>
2052+        """
2053+
2054+        def cb(result):
2055+            self.assertEquals('feature-not-implemented', result.condition)
2056+            self.assertEquals('unsupported', result.appCondition.name)
2057+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2058+            self.assertEquals('config-node', result.appCondition['feature'])
2059+
2060+        d = self.handleRequest(xml)
2061+        self.assertFailure(d, error.StanzaError)
2062+        d.addCallback(cb)
2063+        return d
2064+
2065+
2066+    def test_items(self):
2067+        """
2068+        Non-overridden L{PubSubService.items} yields unsupported error.
2069+        """
2070+        xml = """
2071+        <iq type='get' to='pubsub.example.org'
2072+                       from='user@example.org'>
2073+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2074+            <items node='test'/>
2075+          </pubsub>
2076+        </iq>
2077+        """
2078+
2079+        def cb(result):
2080+            self.assertEquals('feature-not-implemented', result.condition)
2081+            self.assertEquals('unsupported', result.appCondition.name)
2082+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2083+            self.assertEquals('retrieve-items', result.appCondition['feature'])
2084+
2085+        d = self.handleRequest(xml)
2086+        self.assertFailure(d, error.StanzaError)
2087+        d.addCallback(cb)
2088+        return d
2089+
2090+
2091+    def test_retract(self):
2092+        """
2093+        Non-overridden L{PubSubService.retract} yields unsupported error.
2094+        """
2095+        xml = """
2096+        <iq type='set' to='pubsub.example.org'
2097+                       from='user@example.org'>
2098+          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
2099+            <retract node='test'>
2100+              <item id='item1'/>
2101+              <item id='item2'/>
2102+            </retract>
2103+          </pubsub>
2104+        </iq>
2105+        """
2106+
2107+        def cb(result):
2108+            self.assertEquals('feature-not-implemented', result.condition)
2109+            self.assertEquals('unsupported', result.appCondition.name)
2110+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2111+            self.assertEquals('retract-items', result.appCondition['feature'])
2112+
2113+        d = self.handleRequest(xml)
2114+        self.assertFailure(d, error.StanzaError)
2115+        d.addCallback(cb)
2116+        return d
2117+
2118+
2119+    def test_purge(self):
2120+        """
2121+        Non-overridden L{PubSubService.purge} yields unsupported error.
2122+        """
2123+        xml = """
2124+        <iq type='set' to='pubsub.example.org'
2125+                       from='user@example.org'>
2126+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2127+            <purge node='test'/>
2128+          </pubsub>
2129+        </iq>
2130+        """
2131+
2132+        def cb(result):
2133+            self.assertEquals('feature-not-implemented', result.condition)
2134+            self.assertEquals('unsupported', result.appCondition.name)
2135+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2136+            self.assertEquals('purge-nodes', result.appCondition['feature'])
2137+
2138+        d = self.handleRequest(xml)
2139+        self.assertFailure(d, error.StanzaError)
2140+        d.addCallback(cb)
2141+        return d
2142+
2143+
2144+    def test_delete(self):
2145+        """
2146+        Non-overridden L{PubSubService.delete} yields unsupported error.
2147+        """
2148+        xml = """
2149+        <iq type='set' to='pubsub.example.org'
2150+                       from='user@example.org'>
2151+          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
2152+            <delete node='test'/>
2153+          </pubsub>
2154+        </iq>
2155+        """
2156+
2157+        def cb(result):
2158+            self.assertEquals('feature-not-implemented', result.condition)
2159+            self.assertEquals('unsupported', result.appCondition.name)
2160+            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
2161+            self.assertEquals('delete-nodes', result.appCondition['feature'])
2162+
2163+        d = self.handleRequest(xml)
2164+        self.assertFailure(d, error.StanzaError)
2165+        d.addCallback(cb)
2166+        return d
Note: See TracBrowser for help on using the repository browser.