changeset 209:7f3ffb7a1a9e

Add support for node deletion with redirect.
author Ralph Meijer <ralphm@ik.nu>
date Fri, 30 Jan 2009 14:35:05 +0000
parents cc4f45ef793e
children 2a0a6a671776
files idavoll/backend.py idavoll/gateway.py idavoll/test/test_backend.py idavoll/test/test_gateway.py
diffstat 4 files changed, 283 insertions(+), 28 deletions(-) [+]
line wrap: on
line diff
--- a/idavoll/backend.py	Tue Sep 09 14:54:33 2008 +0000
+++ b/idavoll/backend.py	Fri Jan 30 14:35:05 2009 +0000
@@ -434,20 +434,23 @@
         return d
 
 
-    def deleteNode(self, nodeIdentifier, requestor):
+    def deleteNode(self, nodeIdentifier, requestor, redirectURI=None):
         d = self.storage.getNode(nodeIdentifier)
         d.addCallback(_getAffiliation, requestor)
-        d.addCallback(self._doPreDelete)
+        d.addCallback(self._doPreDelete, redirectURI)
         return d
 
 
-    def _doPreDelete(self, result):
+    def _doPreDelete(self, result, redirectURI):
         node, affiliation = result
 
         if affiliation != 'owner':
             raise error.Forbidden()
 
-        d = defer.DeferredList([cb(node.nodeIdentifier)
+        data = {'nodeIdentifier': node.nodeIdentifier,
+                'redirectURI': redirectURI}
+
+        d = defer.DeferredList([cb(data)
                                 for cb in self._callbackList],
                                consumeErrors=1)
         d.addCallback(self._doDelete, node.nodeIdentifier)
@@ -561,11 +564,14 @@
                                                                notifications))
 
 
-    def _preDelete(self, nodeIdentifier):
+    def _preDelete(self, data):
+        nodeIdentifier = data['nodeIdentifier']
+        redirectURI = data.get('redirectURI', None)
         d = self.backend.getSubscribers(nodeIdentifier)
         d.addCallback(lambda subscribers: self.notifyDelete(self.serviceJID,
                                                             nodeIdentifier,
-                                                            subscribers))
+                                                            subscribers,
+                                                            redirectURI))
         return d
 
 
--- a/idavoll/gateway.py	Tue Sep 09 14:54:33 2008 +0000
+++ b/idavoll/gateway.py	Fri Jan 30 14:35:05 2009 +0000
@@ -171,10 +171,7 @@
         Respond to a POST request to create a new node.
         """
 
-        def respond(result):
-            return http.Response(responsecode.NO_CONTENT)
-
-        def getNode():
+        def gotStream(_):
             if request.args.get('uri'):
                 jid, nodeIdentifier = getServiceAndNode(request.args['uri'][0])
                 return defer.succeed(nodeIdentifier)
@@ -182,6 +179,20 @@
                 raise http.HTTPError(http.Response(responsecode.BAD_REQUEST,
                                                    "No URI given"))
 
+        def doDelete(nodeIdentifier, data):
+            if data:
+                params = simplejson.loads(''.join(data))
+                redirectURI = params.get('redirect_uri')
+            else:
+                redirectURI = None
+
+            return self.backend.deleteNode(nodeIdentifier, self.owner,
+                                           redirectURI)
+
+        def respond(result):
+            return http.Response(responsecode.NO_CONTENT)
+
+
         def trapNotFound(failure):
             failure.trap(error.NodeNotFound)
             return http.StatusResponse(responsecode.NOT_FOUND,
@@ -192,8 +203,10 @@
             return http.StatusResponse(responsecode.BAD_REQUEST,
                     "Malformed XMPP URI: %s" % failure.value.message)
 
-        d = getNode()
-        d.addCallback(self.backend.deleteNode, self.owner)
+        data = []
+        d = readStream(request.stream, data.append)
+        d.addCallback(gotStream)
+        d.addCallback(doDelete, data)
         d.addCallback(respond)
         d.addErrback(trapNotFound)
         d.addErrback(trapXMPPURIParseError)
@@ -473,11 +486,14 @@
 
         service = event.sender
         nodeIdentifier = event.nodeIdentifier
-        self.callCallbacks(service, nodeIdentifier, eventType='DELETED')
+        redirectURI = event.redirectURI
+        self.callCallbacks(service, nodeIdentifier, eventType='DELETED',
+                           redirectURI=redirectURI)
 
 
     def _postTo(self, callbacks, service, nodeIdentifier,
-                      payload=None, contentType=None, eventType=None):
+                      payload=None, contentType=None, eventType=None,
+                      redirectURI=None):
 
         if not callbacks:
             return
@@ -495,6 +511,11 @@
         if eventType:
             headers['Event'] = eventType
 
+        if redirectURI:
+            headers['Link'] = '<%s>; rel=alternate' % (
+                              redirectURI.encode('utf-8'),
+                              )
+
         def postNotification(callbackURI):
             d = client.getPage(str(callbackURI),
                                    method='POST',
@@ -507,7 +528,8 @@
 
 
     def callCallbacks(self, service, nodeIdentifier,
-                            payload=None, contentType=None, eventType=None):
+                            payload=None, contentType=None, eventType=None,
+                            redirectURI=None):
 
         def eb(failure):
             failure.trap(error.NoCallbacks)
@@ -516,7 +538,7 @@
 
         d = self.storage.getCallbacks(service, nodeIdentifier)
         d.addCallback(self._postTo, service, nodeIdentifier, payload,
-                                    contentType, eventType)
+                                    contentType, eventType, redirectURI)
         d.addErrback(eb)
         d.addErrback(log.err)
 
@@ -708,7 +730,10 @@
 
     def http_POST(self, request):
         p = WebStreamParser()
-        d = p.parse(request.stream)
+        if not request.headers.hasHeader('Event'):
+            d = p.parse(request.stream)
+        else:
+            d = defer.succeed(None)
         d.addCallback(self.callback, request.headers)
         d.addCallback(lambda _: http.Response(responsecode.NO_CONTENT))
         return d
@@ -769,6 +794,25 @@
         return f.deferred.addCallback(simplejson.loads)
 
 
+    def delete(self, xmppURI, redirectURI=None):
+        query = {'uri': xmppURI}
+
+        if redirectURI:
+            params = {'redirect_uri': redirectURI}
+            postdata = simplejson.dumps(params)
+            headers = {'Content-Type': MIME_JSON}
+        else:
+            postdata = None
+            headers = None
+
+        f = getPageWithFactory(self._makeURI('delete', query),
+                    method='POST',
+                    postdata=postdata,
+                    headers=headers,
+                    agent=self.agent)
+        return f.deferred
+
+
     def publish(self, entry, xmppURI=None):
         query = xmppURI and {'uri': xmppURI}
 
--- a/idavoll/test/test_backend.py	Tue Sep 09 14:54:33 2008 +0000
+++ b/idavoll/test/test_backend.py	Fri Jan 30 14:35:05 2009 +0000
@@ -13,11 +13,12 @@
 from twisted.words.protocols.jabber import jid
 from twisted.words.protocols.jabber.error import StanzaError
 
-from wokkel import pubsub
+from wokkel import iwokkel, pubsub
 
 from idavoll import backend, error, iidavoll
 
 OWNER = jid.JID('owner@example.com')
+SERVICE = jid.JID('test.example.org')
 NS_PUBSUB = 'http://jabber.org/protocol/pubsub'
 
 class BackendTest(unittest.TestCase):
@@ -35,30 +36,34 @@
                     return defer.succeed('owner')
 
         class TestStorage:
+            def __init__(self):
+                self.deleteCalled = []
+
             def getNode(self, nodeIdentifier):
                 return defer.succeed(TestNode())
 
             def deleteNode(self, nodeIdentifier):
                 if nodeIdentifier in ['to-be-deleted']:
-                    self.deleteCalled = True
+                    self.deleteCalled.append(nodeIdentifier)
                     return defer.succeed(None)
                 else:
                     return defer.fail(error.NodeNotFound())
 
-        def preDelete(nodeIdentifier):
-            self.preDeleteCalled = True
+        def preDelete(data):
+            self.assertFalse(self.storage.deleteCalled)
+            preDeleteCalled.append(data)
             return defer.succeed(None)
 
         def cb(result):
-            self.assertTrue(self.preDeleteCalled)
+            self.assertEquals(1, len(preDeleteCalled))
+            data = preDeleteCalled[-1]
+            self.assertEquals('to-be-deleted', data['nodeIdentifier'])
             self.assertTrue(self.storage.deleteCalled)
 
         self.storage = TestStorage()
         self.backend = backend.BackendService(self.storage)
-        self.storage.backend = self.backend
 
-        self.preDeleteCalled = False
-        self.deleteCalled = False
+        preDeleteCalled = []
 
         self.backend.registerPreDelete(preDelete)
         d = self.backend.deleteNode('to-be-deleted', OWNER)
@@ -66,6 +71,52 @@
         return d
 
 
+    def test_deleteNodeRedirect(self):
+        uri = 'xmpp:%s?;node=test2' % (SERVICE.full(),)
+
+        class TestNode:
+            nodeIdentifier = 'to-be-deleted'
+            def getAffiliation(self, entity):
+                if entity is OWNER:
+                    return defer.succeed('owner')
+
+        class TestStorage:
+            def __init__(self):
+                self.deleteCalled = []
+
+            def getNode(self, nodeIdentifier):
+                return defer.succeed(TestNode())
+
+            def deleteNode(self, nodeIdentifier):
+                if nodeIdentifier in ['to-be-deleted']:
+                    self.deleteCalled.append(nodeIdentifier)
+                    return defer.succeed(None)
+                else:
+                    return defer.fail(error.NodeNotFound())
+
+        def preDelete(data):
+            self.assertFalse(self.storage.deleteCalled)
+            preDeleteCalled.append(data)
+            return defer.succeed(None)
+
+        def cb(result):
+            self.assertEquals(1, len(preDeleteCalled))
+            data = preDeleteCalled[-1]
+            self.assertEquals('to-be-deleted', data['nodeIdentifier'])
+            self.assertEquals(uri, data['redirectURI'])
+            self.assertTrue(self.storage.deleteCalled)
+
+        self.storage = TestStorage()
+        self.backend = backend.BackendService(self.storage)
+
+        preDeleteCalled = []
+
+        self.backend.registerPreDelete(preDelete)
+        d = self.backend.deleteNode('to-be-deleted', OWNER, redirectURI=uri)
+        d.addCallback(cb)
+        return d
+
+
     def test_createNodeNoID(self):
         """
         Test creation of a node without a given node identifier.
@@ -397,6 +448,80 @@
 
 class PubSubServiceFromBackendTest(unittest.TestCase):
 
+    def test_interfaceIBackend(self):
+        s = backend.PubSubServiceFromBackend(BaseTestBackend())
+        self.assertTrue(verifyObject(iwokkel.IPubSubService, s))
+
+
+    def test_preDelete(self):
+        """
+        Test pre-delete sending out notifications to subscribers.
+        """
+
+        class TestBackend(BaseTestBackend):
+            preDeleteFn = None
+
+            def registerPreDelete(self, preDeleteFn):
+                self.preDeleteFn = preDeleteFn
+
+            def getSubscribers(self, nodeIdentifier):
+                return defer.succeed([OWNER])
+
+        def notifyDelete(service, nodeIdentifier, subscribers,
+                         redirectURI=None):
+            self.assertEqual(SERVICE, service)
+            self.assertEqual('test', nodeIdentifier)
+            self.assertEqual([OWNER], subscribers)
+            self.assertIdentical(None, redirectURI)
+            d1.callback(None)
+
+        d1 = defer.Deferred()
+        s = backend.PubSubServiceFromBackend(TestBackend())
+        s.serviceJID = SERVICE
+        s.notifyDelete = notifyDelete
+        self.assertTrue(verifyObject(iwokkel.IPubSubService, s))
+        self.assertNotIdentical(None, s.backend.preDeleteFn)
+        data = {'nodeIdentifier': 'test'}
+        d2 = s.backend.preDeleteFn(data)
+        return defer.DeferredList([d1, d2], fireOnOneErrback=1)
+
+
+    def test_preDeleteRedirect(self):
+        """
+        Test pre-delete sending out notifications to subscribers.
+        """
+
+        uri = 'xmpp:%s?;node=test2' % (SERVICE.full(),)
+
+        class TestBackend(BaseTestBackend):
+            preDeleteFn = None
+
+            def registerPreDelete(self, preDeleteFn):
+                self.preDeleteFn = preDeleteFn
+
+            def getSubscribers(self, nodeIdentifier):
+                return defer.succeed([OWNER])
+
+        def notifyDelete(service, nodeIdentifier, subscribers,
+                         redirectURI=None):
+            self.assertEqual(SERVICE, service)
+            self.assertEqual('test', nodeIdentifier)
+            self.assertEqual([OWNER], subscribers)
+            self.assertEqual(uri, redirectURI)
+            d1.callback(None)
+
+        d1 = defer.Deferred()
+        s = backend.PubSubServiceFromBackend(TestBackend())
+        s.serviceJID = SERVICE
+        s.notifyDelete = notifyDelete
+        self.assertTrue(verifyObject(iwokkel.IPubSubService, s))
+        self.assertNotIdentical(None, s.backend.preDeleteFn)
+        data = {'nodeIdentifier': 'test',
+                'redirectURI': uri}
+        d2 = s.backend.preDeleteFn(data)
+        return defer.DeferredList([d1, d2], fireOnOneErrback=1)
+
+
     def test_unsubscribeNotSubscribed(self):
         """
         Test unsubscription request when not subscribed.
@@ -410,7 +535,7 @@
             self.assertEquals('unexpected-request', e.condition)
 
         s = backend.PubSubServiceFromBackend(TestBackend())
-        d = s.unsubscribe(OWNER, 'test.example.org', 'test', OWNER)
+        d = s.unsubscribe(OWNER, SERVICE, 'test', OWNER)
         self.assertFailure(d, StanzaError)
         d.addCallback(cb)
         return d
@@ -435,7 +560,7 @@
             self.assertEquals({'pubsub#persist_items': True}, info['meta-data'])
 
         s = backend.PubSubServiceFromBackend(TestBackend())
-        d = s.getNodeInfo(OWNER, 'test.example.org', 'test')
+        d = s.getNodeInfo(OWNER, SERVICE, 'test')
         d.addCallback(cb)
         return d
 
@@ -469,6 +594,6 @@
             self.assertEquals(True, options["pubsub#persist_items"])
 
         s = backend.PubSubServiceFromBackend(TestBackend())
-        d = s.getDefaultConfiguration(OWNER, 'test.example.org', 'leaf')
+        d = s.getDefaultConfiguration(OWNER, SERVICE, 'leaf')
         d.addCallback(cb)
         return d
--- a/idavoll/test/test_gateway.py	Tue Sep 09 14:54:33 2008 +0000
+++ b/idavoll/test/test_gateway.py	Fri Jan 30 14:35:05 2009 +0000
@@ -97,6 +97,86 @@
         d.addCallback(cb)
         return d
 
+    def test_delete(self):
+        def cb(response):
+            xmppURI = response['uri']
+            d = self.client.delete(xmppURI)
+            return d
+
+        d = self.client.create()
+        d.addCallback(cb)
+        return d
+
+    def test_deleteWithRedirect(self):
+        def cb(response):
+            xmppURI = response['uri']
+            redirectURI = 'xmpp:%s?node=test' % componentJID
+            d = self.client.delete(xmppURI, redirectURI)
+            return d
+
+        d = self.client.create()
+        d.addCallback(cb)
+        return d
+
+    def test_deleteNotification(self):
+        def onNotification(data, headers):
+            try:
+                self.assertTrue(headers.hasHeader('Event'))
+                self.assertEquals(['DELETED'], headers.getRawHeaders('Event'))
+                self.assertFalse(headers.hasHeader('Link'))
+            except:
+                self.client.deferred.errback()
+            else:
+                self.client.deferred.callback(None)
+
+        def cb(response):
+            xmppURI = response['uri']
+            d = self.client.subscribe(xmppURI)
+            d.addCallback(lambda _: xmppURI)
+            return d
+
+        def cb2(xmppURI):
+            d = self.client.delete(xmppURI)
+            return d
+
+        self.client.callback = onNotification
+        self.client.deferred = defer.Deferred()
+        d = self.client.create()
+        d.addCallback(cb)
+        d.addCallback(cb2)
+        return defer.gatherResults([d, self.client.deferred])
+
+    def test_deleteNotificationWithRedirect(self):
+        redirectURI = 'xmpp:%s?node=test' % componentJID
+
+        def onNotification(data, headers):
+            try:
+                self.assertTrue(headers.hasHeader('Event'))
+                self.assertEquals(['DELETED'], headers.getRawHeaders('Event'))
+                self.assertEquals(['<%s>; rel=alternate' % redirectURI],
+                                  headers.getRawHeaders('Link'))
+            except:
+                self.client.deferred.errback()
+            else:
+                self.client.deferred.callback(None)
+
+        def cb(response):
+            xmppURI = response['uri']
+            d = self.client.subscribe(xmppURI)
+            d.addCallback(lambda _: xmppURI)
+            return d
+
+        def cb2(xmppURI):
+            d = self.client.delete(xmppURI, redirectURI)
+            return d
+
+        self.client.callback = onNotification
+        self.client.deferred = defer.Deferred()
+        d = self.client.create()
+        d.addCallback(cb)
+        d.addCallback(cb2)
+        return defer.gatherResults([d, self.client.deferred])
+
     def test_list(self):
         d = self.client.listNodes()
         return d