diff src/test/test_backend.py @ 369:dabee42494ac

config file + cleaning: - SàT Pubsub can now be configured using the same config file as SàT itself (i.e. sat.conf or .sat.conf), in the same locations (/etc, local dir, xdg dir). Its options must be in the "pubsub" section - options on command line override config options - removed tap and http files which are not used anymore - changed directory structure to put source in src, to be coherent with SàT and Libervia - changed options name, db* become db_*, secret become xmpp_pwd - an exception is raised if jid or xmpp_pwd is are not configured
author Goffi <goffi@goffi.org>
date Fri, 02 Mar 2018 12:59:38 +0100
parents sat_pubsub/test/test_backend.py@618a92080812
children aa3a464df605
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/test/test_backend.py	Fri Mar 02 12:59:38 2018 +0100
@@ -0,0 +1,692 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+
+# Copyright (c) 2003-2011 Ralph Meijer
+# Copyright (c) 2012-2018 Jérôme Poisson
+
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+# --
+
+# This program is based on Idavoll (http://idavoll.ik.nu/),
+# originaly written by Ralph Meijer (http://ralphm.net/blog/)
+# It is sublicensed under AGPL v3 (or any later version) as allowed by the original
+# license.
+
+# --
+
+# Here is a copy of the original license:
+
+# Copyright (c) 2003-2011 Ralph Meijer
+
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+"""
+Tests for L{idavoll.backend}.
+"""
+
+from zope.interface import implements
+from zope.interface.verify import verifyObject
+
+from twisted.internet import defer
+from twisted.trial import unittest
+from twisted.words.protocols.jabber import jid
+from twisted.words.protocols.jabber.error import StanzaError
+
+from wokkel import iwokkel, pubsub
+
+from sat_pubsub import backend, error, iidavoll, const
+
+OWNER = jid.JID('owner@example.com')
+OWNER_FULL = jid.JID('owner@example.com/home')
+SERVICE = jid.JID('test.example.org')
+NS_PUBSUB = 'http://jabber.org/protocol/pubsub'
+
+class BackendTest(unittest.TestCase):
+
+    def test_interfaceIBackend(self):
+        self.assertTrue(verifyObject(iidavoll.IBackendService,
+                                     backend.BackendService(None)))
+
+
+    def test_deleteNode(self):
+        class TestNode:
+            nodeIdentifier = 'to-be-deleted'
+            def getAffiliation(self, entity):
+                if entity.userhostJID() == 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['node'].nodeIdentifier)
+            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_FULL)
+        d.addCallback(cb)
+        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.userhostJID() == 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['node'].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.
+        """
+        class TestStorage:
+            def getDefaultConfiguration(self, nodeType):
+                return {}
+
+            def createNode(self, nodeIdentifier, requestor, config):
+                self.nodeIdentifier = nodeIdentifier
+                return defer.succeed(None)
+
+        self.storage = TestStorage()
+        self.backend = backend.BackendService(self.storage)
+        self.storage.backend = self.backend
+
+        def checkID(nodeIdentifier):
+            self.assertNotIdentical(None, nodeIdentifier)
+            self.assertIdentical(self.storage.nodeIdentifier, nodeIdentifier)
+
+        d = self.backend.createNode(None, OWNER_FULL)
+        d.addCallback(checkID)
+        return d
+
+    class NodeStore:
+        """
+        I just store nodes to pose as an L{IStorage} implementation.
+        """
+        def __init__(self, nodes):
+            self.nodes = nodes
+
+        def getNode(self, nodeIdentifier):
+            try:
+                return defer.succeed(self.nodes[nodeIdentifier])
+            except KeyError:
+                return defer.fail(error.NodeNotFound())
+
+
+    def test_getNotifications(self):
+        """
+        Ensure subscribers show up in the notification list.
+        """
+        item = pubsub.Item()
+        sub = pubsub.Subscription('test', OWNER, 'subscribed')
+
+        class TestNode:
+            def getSubscriptions(self, state=None):
+                return [sub]
+
+        def cb(result):
+            self.assertEquals(1, len(result))
+            subscriber, subscriptions, items = result[-1]
+
+            self.assertEquals(OWNER, subscriber)
+            self.assertEquals({sub}, subscriptions)
+            self.assertEquals([item], items)
+
+        self.storage = self.NodeStore({'test': TestNode()})
+        self.backend = backend.BackendService(self.storage)
+        d = self.backend.getNotifications('test', [item])
+        d.addCallback(cb)
+        return d
+
+    def test_getNotificationsRoot(self):
+        """
+        Ensure subscribers to the root node show up in the notification list
+        for leaf nodes.
+
+        This assumes a flat node relationship model with exactly one collection
+        node: the root node. Each leaf node is automatically a child node
+        of the root node.
+        """
+        item = pubsub.Item()
+        subRoot = pubsub.Subscription('', OWNER, 'subscribed')
+
+        class TestNode:
+            def getSubscriptions(self, state=None):
+                return []
+
+        class TestRootNode:
+            def getSubscriptions(self, state=None):
+                return [subRoot]
+
+        def cb(result):
+            self.assertEquals(1, len(result))
+            subscriber, subscriptions, items = result[-1]
+            self.assertEquals(OWNER, subscriber)
+            self.assertEquals({subRoot}, subscriptions)
+            self.assertEquals([item], items)
+
+        self.storage = self.NodeStore({'test': TestNode(),
+                                       '': TestRootNode()})
+        self.backend = backend.BackendService(self.storage)
+        d = self.backend.getNotifications('test', [item])
+        d.addCallback(cb)
+        return d
+
+
+    def test_getNotificationsMultipleNodes(self):
+        """
+        Ensure that entities that subscribe to a leaf node as well as the
+        root node get exactly one notification.
+        """
+        item = pubsub.Item()
+        sub = pubsub.Subscription('test', OWNER, 'subscribed')
+        subRoot = pubsub.Subscription('', OWNER, 'subscribed')
+
+        class TestNode:
+            def getSubscriptions(self, state=None):
+                return [sub]
+
+        class TestRootNode:
+            def getSubscriptions(self, state=None):
+                return [subRoot]
+
+        def cb(result):
+            self.assertEquals(1, len(result))
+            subscriber, subscriptions, items = result[-1]
+
+            self.assertEquals(OWNER, subscriber)
+            self.assertEquals({sub, subRoot}, subscriptions)
+            self.assertEquals([item], items)
+
+        self.storage = self.NodeStore({'test': TestNode(),
+                                       '': TestRootNode()})
+        self.backend = backend.BackendService(self.storage)
+        d = self.backend.getNotifications('test', [item])
+        d.addCallback(cb)
+        return d
+
+
+    def test_getDefaultConfiguration(self):
+        """
+        L{backend.BackendService.getDefaultConfiguration} should return
+        a deferred that fires a dictionary with configuration values.
+        """
+
+        class TestStorage:
+            def getDefaultConfiguration(self, nodeType):
+                return {
+                    "pubsub#persist_items": True,
+                    "pubsub#deliver_payloads": True}
+
+        def cb(options):
+            self.assertIn("pubsub#persist_items", options)
+            self.assertEqual(True, options["pubsub#persist_items"])
+
+        self.backend = backend.BackendService(TestStorage())
+        d = self.backend.getDefaultConfiguration('leaf')
+        d.addCallback(cb)
+        return d
+
+
+    def test_getNodeConfiguration(self):
+        class testNode:
+            nodeIdentifier = 'node'
+            def getConfiguration(self):
+                return {'pubsub#deliver_payloads': True,
+                        'pubsub#persist_items': False}
+
+        class testStorage:
+            def getNode(self, nodeIdentifier):
+                return defer.succeed(testNode())
+
+        def cb(options):
+            self.assertIn("pubsub#deliver_payloads", options)
+            self.assertEqual(True, options["pubsub#deliver_payloads"])
+            self.assertIn("pubsub#persist_items", options)
+            self.assertEqual(False, options["pubsub#persist_items"])
+
+        self.storage = testStorage()
+        self.backend = backend.BackendService(self.storage)
+        self.storage.backend = self.backend
+
+        d = self.backend.getNodeConfiguration('node')
+        d.addCallback(cb)
+        return d
+
+
+    def test_setNodeConfiguration(self):
+        class testNode:
+            nodeIdentifier = 'node'
+            def getAffiliation(self, entity):
+                if entity.userhostJID() == OWNER:
+                    return defer.succeed('owner')
+            def setConfiguration(self, options):
+                self.options = options
+
+        class testStorage:
+            def __init__(self):
+                self.nodes = {'node': testNode()}
+            def getNode(self, nodeIdentifier):
+                return defer.succeed(self.nodes[nodeIdentifier])
+
+        def checkOptions(node):
+            options = node.options
+            self.assertIn("pubsub#deliver_payloads", options)
+            self.assertEqual(True, options["pubsub#deliver_payloads"])
+            self.assertIn("pubsub#persist_items", options)
+            self.assertEqual(False, options["pubsub#persist_items"])
+
+        def cb(result):
+            d = self.storage.getNode('node')
+            d.addCallback(checkOptions)
+            return d
+
+        self.storage = testStorage()
+        self.backend = backend.BackendService(self.storage)
+        self.storage.backend = self.backend
+
+        options = {'pubsub#deliver_payloads': True,
+                   'pubsub#persist_items': False}
+
+        d = self.backend.setNodeConfiguration('node', options, OWNER_FULL)
+        d.addCallback(cb)
+        return d
+
+
+    def test_publishNoID(self):
+        """
+        Test publish request with an item without a node identifier.
+        """
+        class TestNode:
+            nodeType = 'leaf'
+            nodeIdentifier = 'node'
+            def getAffiliation(self, entity):
+                if entity.userhostJID() == OWNER:
+                    return defer.succeed('owner')
+            def getConfiguration(self):
+                return {'pubsub#deliver_payloads': True,
+                        'pubsub#persist_items': False,
+                        const.OPT_PUBLISH_MODEL: const.VAL_PMODEL_OPEN}
+
+        class TestStorage:
+            def getNode(self, nodeIdentifier):
+                return defer.succeed(TestNode())
+
+        def checkID(notification):
+            self.assertNotIdentical(None, notification['items'][0][2]['id'])
+
+        self.storage = TestStorage()
+        self.backend = backend.BackendService(self.storage)
+        self.storage.backend = self.backend
+
+        self.backend.registerNotifier(checkID)
+
+        items = [pubsub.Item()]
+        d = self.backend.publish('node', items, OWNER_FULL)
+        return d
+
+
+    def test_notifyOnSubscription(self):
+        """
+        Test notification of last published item on subscription.
+        """
+        ITEM = "<item xmlns='%s' id='1'/>" % NS_PUBSUB
+
+        class TestNode:
+            implements(iidavoll.ILeafNode)
+            nodeIdentifier = 'node'
+            nodeType = 'leaf'
+            def getAffiliation(self, entity):
+                if entity is OWNER:
+                    return defer.succeed('owner')
+            def getConfiguration(self):
+                return {'pubsub#deliver_payloads': True,
+                        'pubsub#persist_items': False,
+                        'pubsub#send_last_published_item': 'on_sub',
+                        const.OPT_ACCESS_MODEL: const.VAL_AMODEL_OPEN}
+            def getItems(self, authorized_groups, unrestricted, maxItems):
+                return defer.succeed([(ITEM, const.VAL_AMODEL_OPEN, None)])
+            def addSubscription(self, subscriber, state, options):
+                self.subscription = pubsub.Subscription('node', subscriber,
+                                                        state, options)
+                return defer.succeed(None)
+            def getSubscription(self, subscriber):
+                return defer.succeed(self.subscription)
+            def getNodeOwner(self):
+                return defer.succeed(OWNER)
+
+        class TestStorage:
+            def getNode(self, nodeIdentifier):
+                return defer.succeed(TestNode())
+
+        def cb(data):
+            self.assertEquals('node', data['node'].nodeIdentifier)
+            self.assertEquals([ITEM], data['items'])
+            self.assertEquals(OWNER, data['subscription'].subscriber)
+
+        self.storage = TestStorage()
+        self.backend = backend.BackendService(self.storage)
+        self.storage.backend = self.backend
+
+        class Roster(object):
+            def getRoster(self, owner):
+                return {}
+        self.backend.roster = Roster()
+
+        d1 = defer.Deferred()
+        d1.addCallback(cb)
+        self.backend.registerNotifier(d1.callback)
+        d2 = self.backend.subscribe('node', OWNER, OWNER_FULL)
+        return defer.gatherResults([d1, d2])
+
+    test_notifyOnSubscription.timeout = 2
+
+
+
+class BaseTestBackend(object):
+    """
+    Base class for backend stubs.
+    """
+
+    def supportsPublisherAffiliation(self):
+        return True
+
+
+    def supportsOutcastAffiliation(self):
+        return True
+
+
+    def supportsPersistentItems(self):
+        return True
+
+
+    def supportsInstantNodes(self):
+        return True
+
+    def supportsItemAccess(self):
+        return True
+
+    def supportsAutoCreate(self):
+        return True
+
+    def supportsCreatorCheck(self):
+        return True
+
+    def supportsGroupBlog(self):
+        return True
+
+    def registerNotifier(self, observerfn, *args, **kwargs):
+        return
+
+
+    def registerPreDelete(self, preDeleteFn):
+        return
+
+
+
+class PubSubResourceFromBackendTest(unittest.TestCase):
+
+    def test_interface(self):
+        resource = backend.PubSubResourceFromBackend(BaseTestBackend())
+        self.assertTrue(verifyObject(iwokkel.IPubSubResource, resource))
+
+
+    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()
+        resource = backend.PubSubResourceFromBackend(TestBackend())
+        resource.serviceJID = SERVICE
+        resource.pubsubService = pubsub.PubSubService()
+        resource.pubsubService.notifyDelete = notifyDelete
+        self.assertTrue(verifyObject(iwokkel.IPubSubResource, resource))
+        self.assertNotIdentical(None, resource.backend.preDeleteFn)
+        
+        class TestNode:
+            implements(iidavoll.ILeafNode)
+            nodeIdentifier = 'test'
+            nodeType = 'leaf'
+
+        data = {'node': TestNode()}
+        d2 = resource.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()
+        resource = backend.PubSubResourceFromBackend(TestBackend())
+        resource.serviceJID = SERVICE
+        resource.pubsubService = pubsub.PubSubService()
+        resource.pubsubService.notifyDelete = notifyDelete
+        self.assertTrue(verifyObject(iwokkel.IPubSubResource, resource))
+        self.assertNotIdentical(None, resource.backend.preDeleteFn)
+
+        class TestNode:
+            implements(iidavoll.ILeafNode)
+            nodeIdentifier = 'test'
+            nodeType = 'leaf'
+
+        data = {'node': TestNode(),
+                'redirectURI': uri}
+        d2 = resource.backend.preDeleteFn(data)
+        return defer.DeferredList([d1, d2], fireOnOneErrback=1)
+
+
+    def test_unsubscribeNotSubscribed(self):
+        """
+        Test unsubscription request when not subscribed.
+        """
+
+        class TestBackend(BaseTestBackend):
+            def unsubscribe(self, nodeIdentifier, subscriber, requestor):
+                return defer.fail(error.NotSubscribed())
+
+        def cb(e):
+            self.assertEquals('unexpected-request', e.condition)
+
+        resource = backend.PubSubResourceFromBackend(TestBackend())
+        request = pubsub.PubSubRequest()
+        request.sender = OWNER
+        request.recipient = SERVICE
+        request.nodeIdentifier = 'test'
+        request.subscriber = OWNER
+        d = resource.unsubscribe(request)
+        self.assertFailure(d, StanzaError)
+        d.addCallback(cb)
+        return d
+
+
+    def test_getInfo(self):
+        """
+        Test retrieving node information.
+        """
+
+        class TestBackend(BaseTestBackend):
+            def getNodeType(self, nodeIdentifier):
+                return defer.succeed('leaf')
+
+            def getNodeMetaData(self, nodeIdentifier):
+                return defer.succeed({'pubsub#persist_items': True})
+
+        def cb(info):
+            self.assertIn('type', info)
+            self.assertEquals('leaf', info['type'])
+            self.assertIn('meta-data', info)
+            self.assertEquals({'pubsub#persist_items': True}, info['meta-data'])
+
+        resource = backend.PubSubResourceFromBackend(TestBackend())
+        d = resource.getInfo(OWNER, SERVICE, 'test')
+        d.addCallback(cb)
+        return d
+
+
+    def test_getConfigurationOptions(self):
+        class TestBackend(BaseTestBackend):
+            nodeOptions = {
+                    "pubsub#persist_items":
+                        {"type": "boolean",
+                         "label": "Persist items to storage"},
+                    "pubsub#deliver_payloads":
+                        {"type": "boolean",
+                         "label": "Deliver payloads with event notifications"}
+            }
+
+        resource = backend.PubSubResourceFromBackend(TestBackend())
+        options = resource.getConfigurationOptions()
+        self.assertIn("pubsub#persist_items", options)
+
+
+    def test_default(self):
+        class TestBackend(BaseTestBackend):
+            def getDefaultConfiguration(self, nodeType):
+                options = {"pubsub#persist_items": True,
+                           "pubsub#deliver_payloads": True,
+                           "pubsub#send_last_published_item": 'on_sub',
+                }
+                return defer.succeed(options)
+
+        def cb(options):
+            self.assertEquals(True, options["pubsub#persist_items"])
+
+        resource = backend.PubSubResourceFromBackend(TestBackend())
+        request = pubsub.PubSubRequest()
+        request.sender = OWNER
+        request.recipient = SERVICE
+        request.nodeType = 'leaf'
+        d = resource.default(request)
+        d.addCallback(cb)
+        return d