# HG changeset patch # User Ralph Meijer # Date 1233326105 0 # Node ID 7f3ffb7a1a9ea8053c9040ff6796e7154ed6d110 # Parent cc4f45ef793eff99c4a7c123791fdbf2be2fc35c Add support for node deletion with redirect. diff -r cc4f45ef793e -r 7f3ffb7a1a9e idavoll/backend.py --- 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 diff -r cc4f45ef793e -r 7f3ffb7a1a9e idavoll/gateway.py --- 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} diff -r cc4f45ef793e -r 7f3ffb7a1a9e idavoll/test/test_backend.py --- 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 diff -r cc4f45ef793e -r 7f3ffb7a1a9e idavoll/test/test_gateway.py --- 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