source: ralphm-patches/async-observer.patch @ 77:8715d37c78f8

Last change on this file since 77:8715d37c78f8 was 77:8715d37c78f8, checked in by Ralph Meijer <ralphm@…>, 4 years ago

Move classes related to XML Stanzas to their own module, fix for delay.

File size: 38.9 KB
  • wokkel/generic.py

    # HG changeset patch
    # User Ralph Meijer <ralphm@ik.nu>
    # Date 1427898739 -7200
    #      Wed Apr 01 16:32:19 2015 +0200
    # Node ID a7e41ac1ecd9b0df220142142eafd92aa07481c4
    # Parent  56607e5ddb53e3fafbecb41118bd220dea9310c7
    Add decorator for writing async stanza handlers.
    
    diff --git a/wokkel/generic.py b/wokkel/generic.py
    a b  
    77Generic XMPP protocol helpers.
    88"""
    99
     10__all__ = ['parseXml', 'FallbackHandler', 'VersionHandler', 'XmlPipe',
     11           'DeferredXmlStreamFactory', 'prepareIDNName',
     12           'Stanza', 'Request', 'ErrorStanza']
     13
    1014from zope.interface import implements
    1115
    1216from twisted.internet import defer, protocol
    13 from twisted.python import reflect
    1417from twisted.python.deprecate import deprecated
    1518from twisted.python.versions import Version
    16 from twisted.words.protocols.jabber import error, jid, xmlstream
     19from twisted.words.protocols.jabber import error, xmlstream
    1720from twisted.words.protocols.jabber.xmlstream import toResponse
    1821from twisted.words.xish import domish, utility
    1922from twisted.words.xish.xmlstream import BootstrapMixin
    2023
    2124from wokkel.iwokkel import IDisco
     25from wokkel.stanza import Stanza, ErrorStanza, Request
    2226from wokkel.subprotocols import XMPPHandler
    2327
    2428IQ_GET = '/iq[@type="get"]'
     
    4751
    4852
    4953
    50 def stripNamespace(rootElement):
    51     namespace = rootElement.uri
    52 
    53     def strip(element):
    54         if element.uri == namespace:
    55             element.uri = None
    56             if element.defaultUri == namespace:
    57                 element.defaultUri = None
    58             for child in element.elements():
    59                 strip(child)
    60 
    61     if namespace is not None:
    62         strip(rootElement)
    63 
    64     return rootElement
    65 
    66 
    67 
    6854class FallbackHandler(XMPPHandler):
    6955    """
    7056    XMPP subprotocol handler that catches unhandled iq requests.
     
    162148
    163149
    164150
    165 class Stanza(object):
    166     """
    167     Abstract representation of a stanza.
    168 
    169     @ivar sender: The sending entity.
    170     @type sender: L{jid.JID}
    171     @ivar recipient: The receiving entity.
    172     @type recipient: L{jid.JID}
    173     """
    174 
    175     recipient = None
    176     sender = None
    177     stanzaKind = None
    178     stanzaID = None
    179     stanzaType = None
    180 
    181     def __init__(self, recipient=None, sender=None):
    182         self.recipient = recipient
    183         self.sender = sender
    184 
    185 
    186     @classmethod
    187     def fromElement(Class, element):
    188         """
    189         Create a stanza from a L{domish.Element}.
    190         """
    191         stanza = Class()
    192         stanza.parseElement(element)
    193         return stanza
    194 
    195 
    196     def parseElement(self, element):
    197         """
    198         Parse the stanza element.
    199 
    200         This is called with the stanza's element when a L{Stanza} is
    201         created using L{fromElement}. It parses the stanza's core attributes
    202         (addressing, type and id), strips the namespace from the stanza
    203         element for easier transport across streams and passes on
    204         child elements for further parsing.
    205 
    206         Child element parsers are defined by providing a C{childParsers}
    207         attribute on a subclass, as a mapping from (URI, name) to the name
    208         of the handler on C{self}. C{parseElement} will accumulate
    209         C{childParsers} from its class hierarchy, iterate over the child
    210         elements and pass it to matching handlers based on the child element's
    211         URI and name. The special key of C{None} can be used to pass all
    212         child elements to.
    213         """
    214         if element.hasAttribute('from'):
    215             self.sender = jid.internJID(element['from'])
    216         if element.hasAttribute('to'):
    217             self.recipient = jid.internJID(element['to'])
    218         self.stanzaType = element.getAttribute('type')
    219         self.stanzaID = element.getAttribute('id')
    220 
    221         # Save element
    222         stripNamespace(element)
    223         self.element = element
    224 
    225         # accumulate all childHandlers in the class hierarchy of Class
    226         handlers = {}
    227         reflect.accumulateClassDict(self.__class__, 'childParsers', handlers)
    228 
    229         for child in element.elements():
    230             try:
    231                 handler = handlers[child.uri, child.name]
    232             except KeyError:
    233                 try:
    234                     handler = handlers[None]
    235                 except KeyError:
    236                     continue
    237 
    238             getattr(self, handler)(child)
    239 
    240 
    241     def toElement(self):
    242         element = domish.Element((None, self.stanzaKind))
    243         if self.sender is not None:
    244             element['from'] = self.sender.full()
    245         if self.recipient is not None:
    246             element['to'] = self.recipient.full()
    247         if self.stanzaType:
    248             element['type'] = self.stanzaType
    249         if self.stanzaID:
    250             element['id'] = self.stanzaID
    251         return element
    252 
    253 
    254 
    255 class ErrorStanza(Stanza):
    256 
    257     def parseElement(self, element):
    258         Stanza.parseElement(self, element)
    259         self.exception = error.exceptionFromStanza(element)
    260 
    261 
    262 
    263 class Request(Stanza):
    264     """
    265     IQ request stanza.
    266 
    267     This is a base class for IQ get or set stanzas, to be used with
    268     L{wokkel.subprotocols.StreamManager.request}.
    269     """
    270 
    271     stanzaKind = 'iq'
    272     stanzaType = 'get'
    273     timeout = None
    274 
    275     childParsers = {None: 'parseRequest'}
    276 
    277     def __init__(self, recipient=None, sender=None, stanzaType=None):
    278         Stanza.__init__(self, recipient=recipient, sender=sender)
    279         if stanzaType is not None:
    280             self.stanzaType = stanzaType
    281 
    282 
    283     def parseRequest(self, element):
    284         """
    285         Called with the request's child element for parsing.
    286 
    287         When a request instance is created using L{fromElement}, this method
    288         is called with the child element of the iq. Override this method for
    289         parsing the request's payload.
    290         """
    291 
    292 
    293     def toElement(self):
    294         element = Stanza.toElement(self)
    295 
    296         if not self.stanzaID:
    297             element.addUniqueId()
    298             self.stanzaID = element['id']
    299 
    300         return element
    301 
    302 
    303 
    304151class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory):
    305152    protocol = xmlstream.XmlStream
    306153
  • new file wokkel/stanza.py

    diff --git a/wokkel/stanza.py b/wokkel/stanza.py
    new file mode 100644
    - +  
     1# -*- test-case-name: wokkel.test.test_generic -*-
     2#
     3# Copyright (c) Ralph Meijer.
     4# See LICENSE for details.
     5
     6"""
     7XMPP Stanza helpers.
     8"""
     9
     10from twisted.python import reflect
     11from twisted.words.protocols.jabber import error, jid
     12from twisted.words.xish import domish
     13
     14def stripNamespace(rootElement):
     15    namespace = rootElement.uri
     16
     17    def strip(element):
     18        if element.uri == namespace:
     19            element.uri = None
     20            if element.defaultUri == namespace:
     21                element.defaultUri = None
     22            for child in element.elements():
     23                strip(child)
     24
     25    if namespace is not None:
     26        strip(rootElement)
     27
     28    return rootElement
     29
     30
     31
     32class Stanza(object):
     33    """
     34    Abstract representation of a stanza.
     35
     36    @ivar sender: The sending entity.
     37    @type sender: L{jid.JID}
     38    @ivar recipient: The receiving entity.
     39    @type recipient: L{jid.JID}
     40    """
     41
     42    recipient = None
     43    sender = None
     44    stanzaKind = None
     45    stanzaID = None
     46    stanzaType = None
     47
     48    def __init__(self, recipient=None, sender=None):
     49        self.recipient = recipient
     50        self.sender = sender
     51
     52
     53    @classmethod
     54    def fromElement(Class, element):
     55        """
     56        Create a stanza from a L{domish.Element}.
     57        """
     58        stanza = Class()
     59        stanza.parseElement(element)
     60        return stanza
     61
     62
     63    def parseElement(self, element):
     64        """
     65        Parse the stanza element.
     66
     67        This is called with the stanza's element when a L{Stanza} is
     68        created using L{fromElement}. It parses the stanza's core attributes
     69        (addressing, type and id), strips the namespace from the stanza
     70        element for easier transport across streams and passes on
     71        child elements for further parsing.
     72
     73        Child element parsers are defined by providing a C{childParsers}
     74        attribute on a subclass, as a mapping from (URI, name) to the name
     75        of the handler on C{self}. C{parseElement} will accumulate
     76        C{childParsers} from its class hierarchy, iterate over the child
     77        elements and pass it to matching handlers based on the child element's
     78        URI and name. The special key of C{None} can be used to pass all
     79        child elements to.
     80        """
     81        if element.hasAttribute('from'):
     82            self.sender = jid.internJID(element['from'])
     83        if element.hasAttribute('to'):
     84            self.recipient = jid.internJID(element['to'])
     85        self.stanzaType = element.getAttribute('type')
     86        self.stanzaID = element.getAttribute('id')
     87
     88        # Save element
     89        stripNamespace(element)
     90        self.element = element
     91
     92        # accumulate all childHandlers in the class hierarchy of Class
     93        handlers = {}
     94        reflect.accumulateClassDict(self.__class__, 'childParsers', handlers)
     95
     96        for child in element.elements():
     97            try:
     98                handler = handlers[child.uri, child.name]
     99            except KeyError:
     100                try:
     101                    handler = handlers[None]
     102                except KeyError:
     103                    continue
     104
     105            getattr(self, handler)(child)
     106
     107
     108    def toElement(self):
     109        element = domish.Element((None, self.stanzaKind))
     110        if self.sender is not None:
     111            element['from'] = self.sender.full()
     112        if self.recipient is not None:
     113            element['to'] = self.recipient.full()
     114        if self.stanzaType:
     115            element['type'] = self.stanzaType
     116        if self.stanzaID:
     117            element['id'] = self.stanzaID
     118        return element
     119
     120
     121
     122class ErrorStanza(Stanza):
     123
     124    def parseElement(self, element):
     125        Stanza.parseElement(self, element)
     126        self.exception = error.exceptionFromStanza(element)
     127
     128
     129
     130class Request(Stanza):
     131    """
     132    IQ request stanza.
     133
     134    This is a base class for IQ get or set stanzas, to be used with
     135    L{wokkel.subprotocols.StreamManager.request}.
     136    """
     137
     138    stanzaKind = 'iq'
     139    stanzaType = 'get'
     140    timeout = None
     141
     142    childParsers = {None: 'parseRequest'}
     143
     144    def __init__(self, recipient=None, sender=None, stanzaType=None):
     145        Stanza.__init__(self, recipient=recipient, sender=sender)
     146        if stanzaType is not None:
     147            self.stanzaType = stanzaType
     148
     149
     150    def parseRequest(self, element):
     151        """
     152        Called with the request's child element for parsing.
     153
     154        When a request instance is created using L{fromElement}, this method
     155        is called with the child element of the iq. Override this method for
     156        parsing the request's payload.
     157        """
     158
     159
     160    def toElement(self):
     161        element = Stanza.toElement(self)
     162
     163        if not self.stanzaID:
     164            element.addUniqueId()
     165            self.stanzaID = element['id']
     166
     167        return element
     168
     169
     170
  • wokkel/subprotocols.py

    diff --git a/wokkel/subprotocols.py b/wokkel/subprotocols.py
    a b  
    1010__all__ = ['XMPPHandler', 'XMPPHandlerCollection', 'StreamManager',
    1111           'IQHandlerMixin']
    1212
     13from functools import wraps
     14
    1315from zope.interface import implements
    1416
    1517from twisted.internet import defer
     
    2325from twisted.words.xish import xpath
    2426from twisted.words.xish.domish import IElement
    2527
     28from wokkel.stanza import Stanza
     29
    2630deprecatedModuleAttribute(
    2731        Version("Wokkel", 0, 7, 0),
    2832        "Use twisted.words.protocols.jabber.xmlstream.XMPPHandlerCollection "
     
    285289        """
    286290        Handle iq response by firing associated deferred.
    287291        """
     292        stanza = Stanza.fromElement(iq)
    288293        try:
    289             d = self._iqDeferreds[iq["id"]]
     294            d = self._iqDeferreds[(stanza.sender, stanza.stanzaID)]
    290295        except KeyError:
    291296            return
    292297
    293         del self._iqDeferreds[iq["id"]]
     298        del self._iqDeferreds[(stanza.sender, stanza.stanzaID)]
    294299        iq.handled = True
    295         if iq['type'] == 'error':
     300        if stanza.stanzaType == 'error':
    296301            d.errback(error.exceptionFromStanza(iq))
    297302        else:
    298303            d.callback(iq)
     
    356361
    357362        # Set up iq response tracking
    358363        d = defer.Deferred()
    359         self._iqDeferreds[element['id']] = d
     364        self._iqDeferreds[(request.recipient, request.stanzaID)] = d
    360365
    361366        timeout = getattr(request, 'timeout', self.timeout)
    362367
    363368        if timeout is not None:
    364369            def onTimeout():
    365                 del self._iqDeferreds[element['id']]
     370                del self._iqDeferreds[(request.recipient, request.stanzaID)]
    366371                d.errback(xmlstream.TimeoutError("IQ timed out"))
    367372
    368373            call = self._reactor.callLater(timeout, onTimeout)
     
    379384
    380385
    381386
     387def asyncObserver(observer):
     388    """
     389    Decorator for asynchronous stanza observers.
     390
     391    This decorator makes it easier to do proper error handling for stanza
     392    observers and supports deferred results:
     393
     394        >>> class MyHandler(XMPPHandler):
     395        ...    def connectionInitialized(self):
     396        ...       self.xmlstream.addObserver('/message', self.onMessage)
     397        ...    @asyncObserver
     398        ...    def onMessage(self, element):
     399        ...        return False
     400        ...    @asyncObserver
     401        ...    def onMessage(self, element):
     402        ...        return True
     403        ...    @asyncObserver
     404        ...    def onMessage(self, element):
     405        ...        raise NotImplementedError
     406        ...    @asyncObserver
     407        ...    def onMessage(self, element):
     408        ...        return defer.fail(StanzaError('bad-request'))
     409
     410
     411    If the stanza had its C{handled} attribute set to C{True}, it will be
     412    ignored and the observer will not be called.
     413
     414    The return value of the wrapped observer is used to set the C{handled}
     415    attribute, so that handlers may choose to ignore processing the same
     416    stanza. This is expected to be of type C{boolean} or a deferred. In the
     417    latter case, the C{handled} attribute is set to C{True}.
     418
     419    If an exception is raised, or the deferred has its errback called, the
     420    exception is checked for being a L{error.StanzaError}. If so, an error
     421    response is sent. A L{NotImplementedError} will cause an error response
     422    with the condition C{service-unavailable}. Any other exception will cause a
     423    error response of C{internal-server-error} to be sent.
     424
     425    The return value of the deferred is not used.
     426    """
     427    @wraps(observer)
     428    def observe(self, element):
     429        def checkStanzaType(failure):
     430            if element.getAttribute('type') in ('result', 'error'):
     431                log.err(failure, 'Cannot return error in response to a '
     432                                 'response stanza.')
     433            else:
     434                return failure
     435
     436        def trapNotImplemented(failure):
     437            failure.trap(NotImplementedError)
     438            raise error.StanzaError('service-unavailable')
     439
     440        def trapOtherError(failure):
     441            if failure.check(error.StanzaError):
     442                return failure
     443            else:
     444                log.err(failure, "Unhandled error in observer")
     445                raise error.StanzaError('internal-server-error')
     446
     447        def trapStanzaError(failure):
     448            failure.trap(error.StanzaError)
     449            self.send(failure.value.toResponse(element))
     450
     451        if element.handled:
     452            return
     453
     454        try:
     455            result = observer(self, element)
     456        except Exception:
     457            result = defer.fail()
     458
     459        if isinstance(result, defer.Deferred):
     460            result.addErrback(checkStanzaType)
     461            result.addErrback(trapNotImplemented)
     462            result.addErrback(trapOtherError)
     463            result.addErrback(trapStanzaError)
     464            result.addErrback(log.err)
     465
     466        element.handled = bool(result)
     467    return observe
     468
     469
     470
    382471class IQHandlerMixin(object):
    383472    """
    384473    XMPP subprotocol mixin for handle incoming IQ stanzas.
     
    401490
    402491    A typical way to use this mixin, is to set up L{xpath} observers on the
    403492    C{xmlstream} to call handleRequest, for example in an overridden
    404     L{XMPPHandler.connectionMade}. It is likely a good idea to only listen for
    405     incoming iq get and/org iq set requests, and not for any iq, to prevent
    406     hijacking incoming responses to outgoing iq requests. An example:
     493    L{XMPPHandler.connectionInitialized}. It is likely a good idea to only
     494    listen for incoming iq get and/org iq set requests, and not for any iq, to
     495    prevent hijacking incoming responses to outgoing iq requests. An example:
    407496
    408497        >>> QUERY_ROSTER = "/query[@xmlns='jabber:iq:roster']"
    409498        >>> class MyHandler(XMPPHandler, IQHandlerMixin):
    410499        ...    iqHandlers = {"/iq[@type='get']" + QUERY_ROSTER: 'onRosterGet',
    411500        ...                  "/iq[@type='set']" + QUERY_ROSTER: 'onRosterSet'}
    412         ...    def connectionMade(self):
     501        ...    def connectionInitialized(self):
    413502        ...        self.xmlstream.addObserver(
    414503        ...          "/iq[@type='get' or @type='set']" + QUERY_ROSTER,
    415504        ...          self.handleRequest)
     
    425514
    426515    iqHandlers = None
    427516
     517    @asyncObserver
    428518    def handleRequest(self, iq):
    429519        """
    430520        Find a handler and wrap the call for sending a response stanza.
     
    435525            if result:
    436526                if IElement.providedBy(result):
    437527                    response.addChild(result)
    438                 else:
    439                     for element in result:
    440                         response.addChild(element)
    441528
    442529            return response
    443530
    444         def checkNotImplemented(failure):
     531        def trapNotImplemented(failure):
    445532            failure.trap(NotImplementedError)
    446533            raise error.StanzaError('feature-not-implemented')
    447534
    448         def fromStanzaError(failure, iq):
    449             failure.trap(error.StanzaError)
    450             return failure.value.toResponse(iq)
    451 
    452         def fromOtherError(failure, iq):
    453             log.msg("Unhandled error in iq handler:", isError=True)
    454             log.err(failure)
    455             return error.StanzaError('internal-server-error').toResponse(iq)
    456 
    457535        handler = None
    458536        for queryString, method in self.iqHandlers.iteritems():
    459537            if xpath.internQuery(queryString).matches(iq):
     
    461539
    462540        if handler:
    463541            d = defer.maybeDeferred(handler, iq)
     542            d.addCallback(toResult, iq)
     543            d.addCallback(self.send)
    464544        else:
    465545            d = defer.fail(NotImplementedError())
    466546
    467         d.addCallback(toResult, iq)
    468         d.addErrback(checkNotImplemented)
    469         d.addErrback(fromStanzaError, iq)
    470         d.addErrback(fromOtherError, iq)
    471 
    472         d.addCallback(self.send)
    473 
    474         iq.handled = True
     547        d.addErrback(trapNotImplemented)
     548        return d
  • new file wokkel/test/test_stanza.py

    diff --git a/wokkel/test/test_stanza.py b/wokkel/test/test_stanza.py
    new file mode 100644
    - +  
     1# Copyright (c) Ralph Meijer.
     2# See LICENSE for details.
     3
     4from twisted.trial import unittest
     5from twisted.words.protocols.jabber.jid import JID
     6
     7from wokkel import generic
     8
     9NS_VERSION = 'jabber:iq:version'
     10
     11"""
     12Tests for L{wokkel.generic}.
     13"""
     14
     15
     16
     17class StanzaTest(unittest.TestCase):
     18    """
     19    Tests for L{generic.Stanza}.
     20    """
     21
     22    def test_fromElement(self):
     23        xml = """
     24        <message type='chat' from='other@example.org' to='user@example.org'/>
     25        """
     26
     27        stanza = generic.Stanza.fromElement(generic.parseXml(xml))
     28        self.assertEqual('chat', stanza.stanzaType)
     29        self.assertEqual(JID('other@example.org'), stanza.sender)
     30        self.assertEqual(JID('user@example.org'), stanza.recipient)
     31
     32
     33    def test_fromElementChildParser(self):
     34        """
     35        Child elements for which no parser is defined are ignored.
     36        """
     37        xml = """
     38        <message from='other@example.org' to='user@example.org'>
     39          <x xmlns='http://example.org/'/>
     40        </message>
     41        """
     42
     43        class Message(generic.Stanza):
     44            childParsers = {('http://example.org/', 'x'): '_childParser_x'}
     45            elements = []
     46
     47            def _childParser_x(self, element):
     48                self.elements.append(element)
     49
     50        message = Message.fromElement(generic.parseXml(xml))
     51        self.assertEqual(1, len(message.elements))
     52
     53
     54    def test_fromElementChildParserAll(self):
     55        """
     56        Child elements for which no parser is defined are ignored.
     57        """
     58        xml = """
     59        <message from='other@example.org' to='user@example.org'>
     60          <x xmlns='http://example.org/'/>
     61        </message>
     62        """
     63
     64        class Message(generic.Stanza):
     65            childParsers = {None: '_childParser'}
     66            elements = []
     67
     68            def _childParser(self, element):
     69                self.elements.append(element)
     70
     71        message = Message.fromElement(generic.parseXml(xml))
     72        self.assertEqual(1, len(message.elements))
     73
     74
     75    def test_fromElementChildParserUnknown(self):
     76        """
     77        Child elements for which no parser is defined are ignored.
     78        """
     79        xml = """
     80        <message from='other@example.org' to='user@example.org'>
     81          <x xmlns='http://example.org/'/>
     82        </message>
     83        """
     84        generic.Stanza.fromElement(generic.parseXml(xml))
     85
     86
     87
     88
     89class RequestTest(unittest.TestCase):
     90    """
     91    Tests for L{generic.Request}.
     92    """
     93
     94    def setUp(self):
     95        self.request = generic.Request()
     96
     97
     98    def test_requestParser(self):
     99        """
     100        The request's child element is passed to requestParser.
     101        """
     102        xml = """
     103        <iq type='get'>
     104          <query xmlns='jabber:iq:version'/>
     105        </iq>
     106        """
     107
     108        class VersionRequest(generic.Request):
     109            elements = []
     110
     111            def parseRequest(self, element):
     112                self.elements.append((element.uri, element.name))
     113
     114        request = VersionRequest.fromElement(generic.parseXml(xml))
     115        self.assertEqual([(NS_VERSION, 'query')], request.elements)
     116
     117
     118    def test_toElementStanzaKind(self):
     119        """
     120        A request is an iq stanza.
     121        """
     122        element = self.request.toElement()
     123        self.assertIdentical(None, element.uri)
     124        self.assertEquals('iq', element.name)
     125
     126
     127    def test_toElementStanzaType(self):
     128        """
     129        The request has type 'get'.
     130        """
     131        self.assertEquals('get', self.request.stanzaType)
     132        element = self.request.toElement()
     133        self.assertEquals('get', element.getAttribute('type'))
     134
     135
     136    def test_toElementStanzaTypeSet(self):
     137        """
     138        The request has type 'set'.
     139        """
     140        self.request.stanzaType = 'set'
     141        element = self.request.toElement()
     142        self.assertEquals('set', element.getAttribute('type'))
     143
     144
     145    def test_toElementStanzaID(self):
     146        """
     147        A request, when rendered, has an identifier.
     148        """
     149        element = self.request.toElement()
     150        self.assertNotIdentical(None, self.request.stanzaID)
     151        self.assertEquals(self.request.stanzaID, element.getAttribute('id'))
     152
     153
     154    def test_toElementRecipient(self):
     155        """
     156        A request without recipient, has no 'to' attribute.
     157        """
     158        self.request = generic.Request(recipient=JID('other@example.org'))
     159        self.assertEquals(JID('other@example.org'), self.request.recipient)
     160        element = self.request.toElement()
     161        self.assertEquals(u'other@example.org', element.getAttribute('to'))
     162
     163
     164    def test_toElementRecipientNone(self):
     165        """
     166        A request without recipient, has no 'to' attribute.
     167        """
     168        element = self.request.toElement()
     169        self.assertFalse(element.hasAttribute('to'))
     170
     171
     172    def test_toElementSender(self):
     173        """
     174        A request with sender, has a 'from' attribute.
     175        """
     176        self.request = generic.Request(sender=JID('user@example.org'))
     177        self.assertEquals(JID('user@example.org'), self.request.sender)
     178        element = self.request.toElement()
     179        self.assertEquals(u'user@example.org', element.getAttribute('from'))
     180
     181
     182    def test_toElementSenderNone(self):
     183        """
     184        A request without sender, has no 'from' attribute.
     185        """
     186        element = self.request.toElement()
     187        self.assertFalse(element.hasAttribute('from'))
     188
     189
     190    def test_timeoutDefault(self):
     191        """
     192        The default is no timeout.
     193        """
     194        self.assertIdentical(None, self.request.timeout)
     195
     196
     197    def test_stanzaTypeInit(self):
     198        """
     199        If stanzaType is passed in __init__, it overrides the class variable.
     200        """
     201
     202        class SetRequest(generic.Request):
     203            stanzaType = 'set'
     204
     205        request = SetRequest(stanzaType='get')
     206        self.assertEqual('get', request.stanzaType)
     207
     208
     209    def test_stanzaTypeClass(self):
     210        """
     211        If stanzaType is not passed in __init__, the class variable is used.
     212        """
     213
     214        class SetRequest(generic.Request):
     215            stanzaType = 'set'
     216
     217        request = SetRequest()
     218        self.assertEqual('set', request.stanzaType)
  • wokkel/test/test_subprotocols.py

    diff --git a/wokkel/test/test_subprotocols.py b/wokkel/test/test_subprotocols.py
    a b  
    1414from twisted.python import failure
    1515from twisted.words.xish import domish
    1616from twisted.words.protocols.jabber import error, ijabber, xmlstream
     17from twisted.words.protocols.jabber.jid import JID
    1718
    1819from wokkel import generic, subprotocols
    1920
     
    202203        self.transport = proto_helpers.StringTransport()
    203204        self.xmlstream.transport = self.transport
    204205
    205         self.request = IQGetStanza()
     206        self.request = IQGetStanza(recipient=JID('other@example.org'),
     207                                   sender=JID('user@example.org'))
     208
    206209
    207210    def _streamStarted(self):
    208211        """
     
    562565        self._streamStarted()
    563566
    564567        self.streamManager.request(self.request)
    565         expected = u"<iq type='get' id='%s'/>" % self.request.stanzaID
     568        expected = (u"<iq to='other@example.org' from='user@example.org'"
     569                       u" id='%s' type='get'/>" % self.request.stanzaID)
    566570        self.assertEquals(expected, self.transport.value())
    567571
    568572
     
    575579        self.request.stanzaID = None
    576580        self.streamManager.request(self.request)
    577581        self.assertNotIdentical(None, self.request.stanzaID)
    578         expected = u"<iq type='get' id='%s'/>" % self.request.stanzaID
     582        expected = (u"<iq to='other@example.org' from='user@example.org'"
     583                       u" id='%s' type='get'/>" % self.request.stanzaID)
    579584        self.assertEquals(expected, self.transport.value())
    580585
    581586
     
    587592        self.streamManager.addHandler(handler)
    588593
    589594        self.streamManager.request(self.request)
    590         expected = u"<iq type='get' id='test'/>"
     595        expected = (u"<iq to='other@example.org' from='user@example.org'"
     596                       u" id='test' type='get'/>")
    591597
    592598        xs = self.xmlstream
    593599        self.assertEquals("", xs.transport.value())
     
    616622        d.addCallback(cb)
    617623
    618624        xs = self.xmlstream
    619         xs.dataReceived("<iq type='result' id='test'/>")
     625        xs.dataReceived("<iq from='other@example.org' "
     626                            "type='result' id='test'/>")
    620627        return d
    621628
    622629
     
    629636        self.assertFailure(d, error.StanzaError)
    630637
    631638        xs = self.xmlstream
    632         xs.dataReceived("<iq type='error' id='test'/>")
     639        xs.dataReceived("<iq from='other@example.org' "
     640                            "type='error' id='test'/>")
    633641        return d
    634642
    635643
     
    656664        self.assertFalse(getattr(dispatched[-1], 'handled', False))
    657665
    658666
     667    def test_requestUnrelatedSenderResponse(self):
     668        """
     669        Responses with same id, but different sender are ignored.
     670
     671        As stanza identifiers are unique per entity, iq responses must be
     672        tracked by both the stanza ID and the entity the request was sent to.
     673        If a response with the same id as the request is received, but from
     674        a different entity, it is to be ignored.
     675        """
     676        # Set up a fallback handler that checks the stanza's handled attribute.
     677        # If that is set to True, the iq tracker claims to have handled the
     678        # response.
     679        dispatched = []
     680        def cb(iq):
     681            dispatched.append(iq)
     682
     683        self._streamStarted()
     684        self.xmlstream.addObserver("/iq", cb, -1)
     685
     686        d = self.streamManager.request(self.request)
     687
     688        # Receive an untracked iq response
     689        self.xmlstream.dataReceived("<iq from='foo@example.org' "
     690                                        "type='result' id='test'/>")
     691        self.assertEquals(1, len(dispatched))
     692        self.assertFalse(getattr(dispatched[-1], 'handled', False))
     693
     694        # Receive expected response
     695        self.xmlstream.dataReceived("<iq from='other@example.org' "
     696                                        "type='result' id='test'/>")
     697
     698        return d
     699
     700
    659701    def test_requestCleanup(self):
    660702        """
    661703        Test if the deferred associated with an iq request is removed
     
    665707        self._streamStarted()
    666708        d = self.streamManager.request(self.request)
    667709        xs = self.xmlstream
    668         xs.dataReceived("<iq type='result' id='test'/>")
     710        xs.dataReceived("<iq from='other@example.org' "
     711                            "type='result' id='test'/>")
    669712        self.assertNotIn('test', self.streamManager._iqDeferreds)
    670713        return d
    671714
     
    721764        self.request.timeout = 60
    722765        d = self.streamManager.request(self.request)
    723766        self.clock.callLater(1, self.xmlstream.dataReceived,
    724                              "<iq type='result' id='test'/>")
     767                             "<iq from='other@example.org' "
     768                                 "type='result' id='test'/>")
    725769        self.clock.pump([1, 1])
    726770        self.assertFalse(self.clock.calls)
    727771        return d
     
    790834        self.xmlstream.send(obj)
    791835
    792836
     837
     838class AsyncObserverTest(unittest.TestCase):
     839    """
     840    Tests for L{wokkel.subprotocols.asyncObserver}.
     841    """
     842
     843    def setUp(self):
     844        self.output = []
     845
     846
     847    def send(self, element):
     848        self.output.append(element)
     849
     850
     851    def test_handled(self):
     852        """
     853        If the element is marked as handled, it is ignored.
     854        """
     855        called = []
     856
     857        @subprotocols.asyncObserver
     858        def observer(self, element):
     859            called.append(element)
     860
     861        element = domish.Element((None, u'message'))
     862        element.handled = True
     863
     864        observer(self, element)
     865        self.assertFalse(called)
     866        self.assertTrue(element.handled)
     867        self.assertFalse(self.output)
     868
     869
     870    def test_syncFalse(self):
     871        """
     872        If the observer returns False, the element is not marked as handled.
     873        """
     874        @subprotocols.asyncObserver
     875        def observer(self, element):
     876            return False
     877
     878        element = domish.Element((None, u'message'))
     879
     880        observer(self, element)
     881        self.assertFalse(element.handled)
     882        self.assertFalse(self.output)
     883
     884
     885    def test_syncTrue(self):
     886        """
     887        If the observer returns True, the element is marked as handled.
     888        """
     889        @subprotocols.asyncObserver
     890        def observer(self, element):
     891            return True
     892
     893        element = domish.Element((None, u'message'))
     894
     895        observer(self, element)
     896        self.assertTrue(element.handled)
     897        self.assertFalse(self.output)
     898
     899
     900    def test_syncTruthy(self):
     901        """
     902        A truthy value results in element marked as handled.
     903        """
     904        @subprotocols.asyncObserver
     905        def observer(self, element):
     906            return 3
     907
     908        element = domish.Element((None, u'message'))
     909
     910        observer(self, element)
     911        self.assertTrue(element.handled)
     912        self.assertIsInstance(element.handled, bool)
     913
     914
     915    def test_syncNonTruthy(self):
     916        """
     917        A non-truthy value results in element marked as not handled.
     918        """
     919        @subprotocols.asyncObserver
     920        def observer(self, element):
     921            return None
     922
     923        element = domish.Element((None, u'message'))
     924
     925        observer(self, element)
     926        self.assertFalse(element.handled)
     927        self.assertIsInstance(element.handled, bool)
     928
     929
     930    def test_syncNotImplemented(self):
     931        """
     932        NotImplementedError causes a service-unavailable response.
     933        """
     934        @subprotocols.asyncObserver
     935        def observer(self, element):
     936            raise NotImplementedError()
     937
     938        element = domish.Element((None, u'message'))
     939
     940        observer(self, element)
     941        self.assertTrue(element.handled)
     942        self.assertEquals(1, len(self.output))
     943        exc = error.exceptionFromStanza(self.output[-1])
     944        self.assertEquals(u'service-unavailable', exc.condition)
     945
     946
     947    def test_syncStanzaError(self):
     948        """
     949        A StanzaError is sent back as is.
     950        """
     951        @subprotocols.asyncObserver
     952        def observer(self, element):
     953            raise error.StanzaError(u'forbidden')
     954
     955        element = domish.Element((None, u'message'))
     956
     957        observer(self, element)
     958        self.assertTrue(element.handled)
     959        self.assertEquals(1, len(self.output))
     960        exc = error.exceptionFromStanza(self.output[-1])
     961        self.assertEquals(u'forbidden', exc.condition)
     962
     963
     964    def test_syncOtherError(self):
     965        """
     966        Other exceptions are logged and cause an internal-service response.
     967        """
     968        class Error(Exception):
     969            pass
     970
     971        @subprotocols.asyncObserver
     972        def observer(self, element):
     973            raise Error(u"oops")
     974
     975        element = domish.Element((None, u'message'))
     976
     977        observer(self, element)
     978        self.assertTrue(element.handled)
     979        self.assertEquals(1, len(self.output))
     980        exc = error.exceptionFromStanza(self.output[-1])
     981        self.assertEquals(u'internal-server-error', exc.condition)
     982        self.assertEquals(1, len(self.flushLoggedErrors()))
     983
     984
     985    def test_asyncError(self):
     986        """
     987        Other exceptions are logged and cause an internal-service response.
     988        """
     989        class Error(Exception):
     990            pass
     991
     992        @subprotocols.asyncObserver
     993        def observer(self, element):
     994            return defer.fail(Error("oops"))
     995
     996        element = domish.Element((None, u'message'))
     997
     998        observer(self, element)
     999        self.assertTrue(element.handled)
     1000        self.assertEquals(1, len(self.output))
     1001        exc = error.exceptionFromStanza(self.output[-1])
     1002        self.assertEquals(u'internal-server-error', exc.condition)
     1003        self.assertEquals(1, len(self.flushLoggedErrors()))
     1004
     1005
     1006    def test_errorMessage(self):
     1007        """
     1008        If the element is an error message, observer exceptions are just logged.
     1009        """
     1010        class Error(Exception):
     1011            pass
     1012
     1013        @subprotocols.asyncObserver
     1014        def observer(self, element):
     1015            raise Error("oops")
     1016
     1017        element = domish.Element((None, u'message'))
     1018        element[u'type'] = u'error'
     1019
     1020        observer(self, element)
     1021        self.assertTrue(element.handled)
     1022        self.assertEquals(0, len(self.output))
     1023        self.assertEquals(1, len(self.flushLoggedErrors()))
     1024
     1025
     1026
    7931027class IQHandlerTest(unittest.TestCase):
     1028    """
     1029    Tests for L{subprotocols.IQHandler}.
     1030    """
    7941031
    7951032    def test_match(self):
    7961033        """
    797         Test that the matching handler gets called.
     1034        The matching handler gets called.
    7981035        """
    799 
    8001036        class Handler(DummyIQHandler):
    8011037            called = False
    8021038
     
    8101046        handler.handleRequest(iq)
    8111047        self.assertTrue(handler.called)
    8121048
     1049
    8131050    def test_noMatch(self):
    8141051        """
    815         Test that the matching handler gets called.
     1052        If the element does not match the handler is not called.
    8161053        """
    817 
    8181054        class Handler(DummyIQHandler):
    8191055            called = False
    8201056
     
    8281064        handler.handleRequest(iq)
    8291065        self.assertFalse(handler.called)
    8301066
     1067
    8311068    def test_success(self):
    8321069        """
    833         Test response when the request is handled successfully.
     1070        If None is returned, an empty result iq is returned.
    8341071        """
    835 
    8361072        class Handler(DummyIQHandler):
    8371073            def onGet(self, iq):
    8381074                return None
     
    8471083        self.assertEquals('iq', response.name)
    8481084        self.assertEquals('result', response['type'])
    8491085
     1086
    8501087    def test_successPayload(self):
    8511088        """
    852         Test response when the request is handled successfully with payload.
     1089        If an Element is returned it is added as the payload of the result iq.
    8531090        """
    854 
    8551091        class Handler(DummyIQHandler):
    8561092            payload = domish.Element(('testns', 'foo'))
    8571093
     
    8701106        payload = response.elements().next()
    8711107        self.assertEqual(handler.payload, payload)
    8721108
     1109
    8731110    def test_successDeferred(self):
    8741111        """
    875         Test response when where the handler was a deferred.
     1112        A deferred result is used when fired.
    8761113        """
    877 
    8781114        class Handler(DummyIQHandler):
    8791115            def onGet(self, iq):
    8801116                return defer.succeed(None)
     
    8891125        self.assertEquals('iq', response.name)
    8901126        self.assertEquals('result', response['type'])
    8911127
     1128
    8921129    def test_failure(self):
    8931130        """
    894         Test response when the request is handled unsuccessfully.
     1131        A raised StanzaError causes an error response.
    8951132        """
    896 
    8971133        class Handler(DummyIQHandler):
    8981134            def onGet(self, iq):
    8991135                raise error.StanzaError('forbidden')
     
    9101146        e = error.exceptionFromStanza(response)
    9111147        self.assertEquals('forbidden', e.condition)
    9121148
     1149
    9131150    def test_failureUnknown(self):
    9141151        """
    915         Test response when the request handler raises a non-stanza-error.
     1152        Any Exception cause an internal-server-error response.
    9161153        """
    917 
    9181154        class TestError(Exception):
    9191155            pass
    9201156
     
    9351171        self.assertEquals('internal-server-error', e.condition)
    9361172        self.assertEquals(1, len(self.flushLoggedErrors(TestError)))
    9371173
     1174
    9381175    def test_notImplemented(self):
    9391176        """
    940         Test response when the request is recognised but not implemented.
     1177        A NotImplementedError causes a feature-not-implemented response.
    9411178        """
    942 
    9431179        class Handler(DummyIQHandler):
    9441180            def onGet(self, iq):
    9451181                raise NotImplementedError()
     
    9561192        e = error.exceptionFromStanza(response)
    9571193        self.assertEquals('feature-not-implemented', e.condition)
    9581194
     1195
    9591196    def test_noHandler(self):
    9601197        """
    961         Test when the request is not recognised.
     1198        A missing handler causes a feature-not-implemented response.
    9621199        """
    963 
    9641200        iq = domish.Element((None, 'iq'))
    9651201        iq['type'] = 'set'
    9661202        iq['id'] = 'r1'
Note: See TracBrowser for help on using the repository browser.