Changeset 46:4ee1f9c08b22 for wokkel


Ignore:
Timestamp:
Dec 26, 2008, 2:40:11 PM (13 years ago)
Author:
Ralph Meijer <ralphm@…>
Branch:
default
Convert:
svn:b33ecbfc-034c-dc11-8662-000475d9059e/trunk@140
Message:

If provided, return NodeID in the response for Service Discovery requests.

This also renames the namespace constants to include DISCO in their name
and provides better tests and docstrings.

Author: ralphm.
Reviewer: tofu.
Fixes #7.

Location:
wokkel
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • wokkel/disco.py

    r31 r46  
    1818from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
    1919
    20 NS = 'http://jabber.org/protocol/disco'
    21 NS_INFO = NS + '#info'
    22 NS_ITEMS = NS + '#items'
     20NS_DISCO = 'http://jabber.org/protocol/disco'
     21NS_DISCO_INFO = NS_DISCO + '#info'
     22NS_DISCO_ITEMS = NS_DISCO + '#items'
    2323
    2424IQ_GET = '/iq[@type="get"]'
    25 DISCO_INFO = IQ_GET + '/query[@xmlns="' + NS_INFO + '"]'
    26 DISCO_ITEMS = IQ_GET + '/query[@xmlns="' + NS_ITEMS + '"]'
     25DISCO_INFO = IQ_GET + '/query[@xmlns="' + NS_DISCO_INFO + '"]'
     26DISCO_ITEMS = IQ_GET + '/query[@xmlns="' + NS_DISCO_ITEMS + '"]'
    2727
    2828class DiscoFeature(domish.Element):
     
    3232
    3333    def __init__(self, feature):
    34         domish.Element.__init__(self, (NS_INFO, 'feature'),
     34        domish.Element.__init__(self, (NS_DISCO_INFO, 'feature'),
    3535                                attribs={'var': feature})
    3636
     
    4242
    4343    def __init__(self, category, type, name = None):
    44         domish.Element.__init__(self, (NS_INFO, 'identity'),
     44        domish.Element.__init__(self, (NS_DISCO_INFO, 'identity'),
    4545                                attribs={'category': category,
    4646                                         'type': type})
     
    5555
    5656    def __init__(self, jid, node='', name=None):
    57         domish.Element.__init__(self, (NS_ITEMS, 'item'),
     57        domish.Element.__init__(self, (NS_DISCO_ITEMS, 'item'),
    5858                                attribs={'jid': jid.full()})
    5959        if node:
     
    6464
    6565
     66
    6667class DiscoHandler(XMPPHandler, IQHandlerMixin):
    6768    """
     
    8081        self.xmlstream.addObserver(DISCO_ITEMS, self.handleRequest)
    8182
    82     def _error(self, failure):
    83         failure.trap(defer.FirstError)
    84         return failure.value.subFailure
    8583
    8684    def _onDiscoInfo(self, iq):
     85        """
     86        Called for incoming disco info requests.
     87
     88        @param iq: The request iq element.
     89        @type iq: L{Element<twisted.words.xish.domish.Element>}
     90        """
    8791        requestor = jid.internJID(iq["from"])
    8892        target = jid.internJID(iq["to"])
    8993        nodeIdentifier = iq.query.getAttribute("node", '')
    9094
    91         def toResponse(results):
    92             info = []
    93             for i in results:
    94                 info.extend(i[1])
    95 
     95        def toResponse(info):
    9696            if nodeIdentifier and not info:
    9797                raise error.StanzaError('item-not-found')
    9898            else:
    99                 response = domish.Element((NS_INFO, 'query'))
     99                response = domish.Element((NS_DISCO_INFO, 'query'))
     100                if nodeIdentifier:
     101                    response['node'] = nodeIdentifier
    100102
    101103                for item in info:
     
    104106            return response
    105107
    106         dl = []
    107         for handler in self.parent:
    108             if IDisco.providedBy(handler):
    109                 dl.append(handler.getDiscoInfo(requestor, target,
    110                                                nodeIdentifier))
    111 
    112         d = defer.DeferredList(dl, fireOnOneErrback=1, consumeErrors=1)
    113         d.addCallbacks(toResponse, self._error)
     108        d = self.info(requestor, target, nodeIdentifier)
     109        d.addCallback(toResponse)
    114110        return d
    115111
     112
    116113    def _onDiscoItems(self, iq):
     114        """
     115        Called for incoming disco items requests.
     116
     117        @param iq: The request iq element.
     118        @type iq: L{Element<twisted.words.xish.domish.Element>}
     119        """
    117120        requestor = jid.internJID(iq["from"])
    118121        target = jid.internJID(iq["to"])
    119122        nodeIdentifier = iq.query.getAttribute("node", '')
    120123
    121         def toResponse(results):
    122             items = []
    123             for i in results:
    124                 items.extend(i[1])
    125 
    126             response = domish.Element((NS_ITEMS, 'query'))
     124        def toResponse(items):
     125            response = domish.Element((NS_DISCO_ITEMS, 'query'))
     126            if nodeIdentifier:
     127                response['node'] = nodeIdentifier
    127128
    128129            for item in items:
     
    131132            return response
    132133
    133         dl = []
    134         for handler in self.parent:
    135             if IDisco.providedBy(handler):
    136                 dl.append(handler.getDiscoItems(requestor, target,
    137                                                 nodeIdentifier))
    138 
    139         d = defer.DeferredList(dl, fireOnOneErrback=1, consumeErrors=1)
    140         d.addCallbacks(toResponse, self._error)
     134        d = self.items(requestor, target, nodeIdentifier)
     135        d.addCallback(toResponse)
    141136        return d
     137
     138
     139    def _gatherResults(self, deferredList):
     140        """
     141        Gather results from a list of deferreds.
     142
     143        Similar to L{defer.gatherResults}, but flattens the returned results,
     144        consumes errors after the first one and fires the errback of the
     145        returned deferred with the failure of the first deferred that fires its
     146        errback.
     147
     148        @param deferredList: List of deferreds for which the results should be
     149                             gathered.
     150        @type deferredList: C{list}
     151        @return: Deferred that fires with a list of gathered results.
     152        @rtype: L{defer.Deferred}
     153        """
     154        def cb(resultList):
     155            results = []
     156            for success, value in resultList:
     157                results.extend(value)
     158            return results
     159
     160        def eb(failure):
     161            failure.trap(defer.FirstError)
     162            return failure.value.subFailure
     163
     164        d = defer.DeferredList(deferredList, fireOnOneErrback=1,
     165                                             consumeErrors=1)
     166        d.addCallbacks(cb, eb)
     167        return d
     168
     169
     170    def info(self, requestor, target, nodeIdentifier):
     171        """
     172        Inspect all sibling protocol handlers for disco info.
     173
     174        Calls the L{getDiscoInfo<IDisco.getDiscoInfo>} method on all child
     175        handlers of the parent, that provide L{IDisco}.
     176
     177        @param requestor: The entity that sent the request.
     178        @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
     179        @param target: The entity the request was sent to.
     180        @type target: L{JID<twisted.words.protocols.jabber.jid.JID>}
     181        @param nodeIdentifier: The optional node being queried, or C{''}.
     182        @type nodeIdentifier: C{unicode}
     183        @return: Deferred with the gathered results from sibling handlers.
     184        @rtype: L{defer.Deferred}
     185        """
     186        dl = [handler.getDiscoInfo(requestor, target, nodeIdentifier)
     187              for handler in self.parent
     188              if IDisco.providedBy(handler)]
     189        return self._gatherResults(dl)
     190
     191
     192    def items(self, requestor, target, nodeIdentifier):
     193        """
     194        Inspect all sibling protocol handlers for disco items.
     195
     196        Calls the L{getDiscoItems<IDisco.getDiscoItems>} method on all child
     197        handlers of the parent, that provide L{IDisco}.
     198
     199        @param requestor: The entity that sent the request.
     200        @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
     201        @param target: The entity the request was sent to.
     202        @type target: L{JID<twisted.words.protocols.jabber.jid.JID>}
     203        @param nodeIdentifier: The optional node being queried, or C{''}.
     204        @type nodeIdentifier: C{unicode}
     205        @return: Deferred with the gathered results from sibling handlers.
     206        @rtype: L{defer.Deferred}
     207        """
     208        dl = [handler.getDiscoItems(requestor, target, nodeIdentifier)
     209              for handler in self.parent
     210              if IDisco.providedBy(handler)]
     211        return self._gatherResults(dl)
  • wokkel/test/helpers.py

    r10 r46  
    66"""
    77
     8from twisted.internet import defer
     9from twisted.words.xish import xpath
    810from twisted.words.xish.utility import EventDispatcher
     11
     12from wokkel.generic import parseXml
    913
    1014class XmlStreamStub(object):
     
    5559        """
    5660        self.xmlstream.dispatch(obj)
     61
     62
     63class TestableRequestHandlerMixin(object):
     64    """
     65    Mixin for testing XMPPHandlers that process iq requests.
     66
     67    Handlers that use L{wokkel.subprotocols.IQHandlerMixin} define a
     68    C{iqHandlers} attribute that lists the handlers to be called for iq
     69    requests. This mixin provides L{handleRequest} to mimic the handler
     70    processing for easier testing.
     71    """
     72
     73    def handleRequest(self, xml):
     74        """
     75        Find a handler and call it directly.
     76
     77        @param xml: XML stanza that may yield a handler being called.
     78        @type xml: C{str}.
     79        @return: Deferred that fires with the result of a handler for this
     80                 stanza. If no handler was found, the deferred has its errback
     81                 called with a C{NotImplementedError} exception.
     82        """
     83        handler = None
     84        iq = parseXml(xml)
     85        for queryString, method in self.service.iqHandlers.iteritems():
     86            if xpath.internQuery(queryString).matches(iq):
     87                handler = getattr(self.service, method)
     88
     89        if handler:
     90            d = defer.maybeDeferred(handler, iq)
     91        else:
     92            d = defer.fail(NotImplementedError())
     93
     94        return d
  • wokkel/test/test_disco.py

    r31 r46  
    88from twisted.internet import defer
    99from twisted.trial import unittest
    10 from twisted.words.xish.xmlstream import XmlStreamFactory
     10from twisted.words.protocols.jabber.jid import JID
    1111from zope.interface import implements
    1212
    13 from wokkel.subprotocols import XMPPHandler, StreamManager
    14 
    1513from wokkel import disco
     14from wokkel.subprotocols import XMPPHandler
     15from wokkel.test.helpers import TestableRequestHandlerMixin
     16
    1617
    1718NS_DISCO_INFO = 'http://jabber.org/protocol/disco#info'
    1819NS_DISCO_ITEMS = 'http://jabber.org/protocol/disco#items'
    1920
    20 class DiscoResponder(XMPPHandler):
    21     implements(disco.IDisco)
    22 
    23     def getDiscoInfo(self, requestor, target, nodeIdentifier):
    24         if not nodeIdentifier:
     21
     22class DiscoHandlerTest(unittest.TestCase, TestableRequestHandlerMixin):
     23    """
     24    Tests for L{disco.DiscoHandler}.
     25    """
     26
     27    def setUp(self):
     28        self.service = disco.DiscoHandler()
     29
     30
     31    def test_onDiscoInfo(self):
     32        """
     33        C{onDiscoInfo} should process an info request and return a response.
     34
     35        The request should be parsed, C{info} called with the extracted
     36        parameters, and then the result should be formatted into a proper
     37        response element.
     38        """
     39        xml = """<iq from='test@example.com' to='example.com'
     40                     type='get'>
     41                   <query xmlns='%s'/>
     42                 </iq>""" % NS_DISCO_INFO
     43
     44        def cb(element):
     45            self.assertEqual('query', element.name)
     46            self.assertEqual(NS_DISCO_INFO, element.uri)
     47            self.assertEqual(NS_DISCO_INFO, element.identity.uri)
     48            self.assertEqual('dummy', element.identity['category'])
     49            self.assertEqual('generic', element.identity['type'])
     50            self.assertEqual('Generic Dummy Entity', element.identity['name'])
     51            self.assertEqual(NS_DISCO_INFO, element.feature.uri)
     52            self.assertEqual('jabber:iq:version', element.feature['var'])
     53
     54        def info(requestor, target, nodeIdentifier):
     55            self.assertEqual(JID('test@example.com'), requestor)
     56            self.assertEqual(JID('example.com'), target)
     57            self.assertEqual('', nodeIdentifier)
     58
    2559            return defer.succeed([
    2660                disco.DiscoIdentity('dummy', 'generic', 'Generic Dummy Entity'),
    2761                disco.DiscoFeature('jabber:iq:version')
    2862            ])
    29         else:
    30             return defer.succeed([])
    31 
    32 class DiscoHandlerTest(unittest.TestCase):
    33     def test_DiscoInfo(self):
    34         factory = XmlStreamFactory()
    35         sm = StreamManager(factory)
    36         disco.DiscoHandler().setHandlerParent(sm)
    37         DiscoResponder().setHandlerParent(sm)
    38         xs = factory.buildProtocol(None)
    39         output = []
    40         xs.send = output.append
    41         xs.connectionMade()
    42         xs.dispatch(xs, "//event/stream/authd")
    43         xs.dataReceived("<stream>")
    44         xs.dataReceived("""<iq from='test@example.com' to='example.com'
    45                                type='get'>
    46                              <query xmlns='%s'/>
    47                            </iq>""" % NS_DISCO_INFO)
    48         reply = output[0]
    49         self.assertEqual(NS_DISCO_INFO, reply.query.uri)
    50         self.assertEqual(NS_DISCO_INFO, reply.query.identity.uri)
    51         self.assertEqual('dummy', reply.query.identity['category'])
    52         self.assertEqual('generic', reply.query.identity['type'])
    53         self.assertEqual('Generic Dummy Entity', reply.query.identity['name'])
    54         self.assertEqual(NS_DISCO_INFO, reply.query.feature.uri)
    55         self.assertEqual('jabber:iq:version', reply.query.feature['var'])
    56 
     63
     64        self.service.info = info
     65        d = self.handleRequest(xml)
     66        d.addCallback(cb)
     67        return d
     68
     69    def test_onDiscoInfoWithNode(self):
     70        """
     71        An info request for a node should return it in the response.
     72        """
     73        xml = """<iq from='test@example.com' to='example.com'
     74                     type='get'>
     75                   <query xmlns='%s' node='test'/>
     76                 </iq>""" % NS_DISCO_INFO
     77
     78        def cb(element):
     79            self.assertTrue(element.hasAttribute('node'))
     80            self.assertEqual('test', element['node'])
     81
     82        def info(requestor, target, nodeIdentifier):
     83            self.assertEqual('test', nodeIdentifier)
     84
     85            return defer.succeed([
     86                disco.DiscoFeature('jabber:iq:version')
     87            ])
     88
     89        self.service.info = info
     90        d = self.handleRequest(xml)
     91        d.addCallback(cb)
     92        return d
     93
     94
     95    def test_onDiscoItems(self):
     96        """
     97        C{onDiscoItems} should process an items request and return a response.
     98
     99        The request should be parsed, C{items} called with the extracted
     100        parameters, and then the result should be formatted into a proper
     101        response element.
     102        """
     103        xml = """<iq from='test@example.com' to='example.com'
     104                     type='get'>
     105                   <query xmlns='%s'/>
     106                 </iq>""" % NS_DISCO_ITEMS
     107
     108        def cb(element):
     109            self.assertEqual('query', element.name)
     110            self.assertEqual(NS_DISCO_ITEMS, element.uri)
     111            self.assertEqual(NS_DISCO_ITEMS, element.item.uri)
     112            self.assertEqual('example.com', element.item['jid'])
     113            self.assertEqual('test', element.item['node'])
     114            self.assertEqual('Test node', element.item['name'])
     115
     116        def items(requestor, target, nodeIdentifier):
     117            self.assertEqual(JID('test@example.com'), requestor)
     118            self.assertEqual(JID('example.com'), target)
     119            self.assertEqual('', nodeIdentifier)
     120
     121            return defer.succeed([
     122                disco.DiscoItem(JID('example.com'), 'test', 'Test node'),
     123            ])
     124
     125        self.service.items = items
     126        d = self.handleRequest(xml)
     127        d.addCallback(cb)
     128        return d
     129
     130    def test_onDiscoItemsWithNode(self):
     131        """
     132        An items request for a node should return it in the response.
     133        """
     134        xml = """<iq from='test@example.com' to='example.com'
     135                     type='get'>
     136                   <query xmlns='%s' node='test'/>
     137                 </iq>""" % NS_DISCO_ITEMS
     138
     139        def cb(element):
     140            self.assertTrue(element.hasAttribute('node'))
     141            self.assertEqual('test', element['node'])
     142
     143        def items(requestor, target, nodeIdentifier):
     144            self.assertEqual('test', nodeIdentifier)
     145
     146            return defer.succeed([
     147                disco.DiscoFeature('jabber:iq:version')
     148            ])
     149
     150        self.service.items = items
     151        d = self.handleRequest(xml)
     152        d.addCallback(cb)
     153        return d
     154
     155
     156    def test_info(self):
     157        """
     158        C{info} should gather disco info from sibling handlers.
     159        """
     160        discoItems = [disco.DiscoIdentity('dummy', 'generic',
     161                                          'Generic Dummy Entity'),
     162                      disco.DiscoFeature('jabber:iq:version')
     163        ]
     164
     165        class DiscoResponder(XMPPHandler):
     166            implements(disco.IDisco)
     167
     168            def getDiscoInfo(self, requestor, target, nodeIdentifier):
     169                if not nodeIdentifier:
     170                    return defer.succeed(discoItems)
     171                else:
     172                    return defer.succeed([])
     173
     174        def cb(result):
     175            self.assertEquals(discoItems, result)
     176
     177        self.service.parent = [self.service, DiscoResponder()]
     178        d = self.service.info(JID('test@example.com'), JID('example.com'), '')
     179        d.addCallback(cb)
     180        return d
     181
     182
     183    def test_items(self):
     184        """
     185        C{info} should gather disco items from sibling handlers.
     186        """
     187        discoItems = [disco.DiscoItem(JID('example.com'), 'test', 'Test node')]
     188
     189        class DiscoResponder(XMPPHandler):
     190            implements(disco.IDisco)
     191
     192            def getDiscoItems(self, requestor, target, nodeIdentifier):
     193                if not nodeIdentifier:
     194                    return defer.succeed(discoItems)
     195                else:
     196                    return defer.succeed([])
     197
     198        def cb(result):
     199            self.assertEquals(discoItems, result)
     200
     201        self.service.parent = [self.service, DiscoResponder()]
     202        d = self.service.items(JID('test@example.com'), JID('example.com'), '')
     203        d.addCallback(cb)
     204        return d
  • wokkel/test/test_pubsub.py

    r43 r46  
    1010from twisted.trial import unittest
    1111from twisted.internet import defer
    12 from twisted.words.xish import domish, xpath
     12from twisted.words.xish import domish
    1313from twisted.words.protocols.jabber import error
    1414from twisted.words.protocols.jabber.jid import JID
    1515
    1616from wokkel import data_form, iwokkel, pubsub, shim
    17 from wokkel.generic import parseXml
    18 from wokkel.test.helpers import XmlStreamStub
     17from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
    1918
    2019try:
     
    492491
    493492
    494 class PubSubServiceTest(unittest.TestCase):
     493class PubSubServiceTest(unittest.TestCase, TestableRequestHandlerMixin):
    495494    """
    496495    Tests for L{pubsub.PubSubService}.
     
    501500        self.service = pubsub.PubSubService()
    502501        self.service.send = self.stub.xmlstream.send
    503 
    504     def handleRequest(self, xml):
    505         """
    506         Find a handler and call it directly
    507         """
    508         handler = None
    509         iq = parseXml(xml)
    510         for queryString, method in self.service.iqHandlers.iteritems():
    511             if xpath.internQuery(queryString).matches(iq):
    512                 handler = getattr(self.service, method)
    513 
    514         if handler:
    515             d = defer.maybeDeferred(handler, iq)
    516         else:
    517             d = defer.fail(NotImplementedError())
    518 
    519         return d
    520 
    521502
    522503    def test_interface(self):
Note: See TracChangeset for help on using the changeset viewer.