Changeset 77:8715d37c78f8 in ralphm-patches


Ignore:
Timestamp:
Jun 22, 2016, 9:15:55 AM (4 years ago)
Author:
Ralph Meijer <ralphm@…>
Branch:
default
Message:

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

Files:
1 added
2 edited

Legend:

Unmodified
Added
Removed
  • async-observer.patch

    r76 r77  
    11# HG changeset patch
    2 # Parent 56607e5ddb53e3fafbecb41118bd220dea9310c7
     2# User Ralph Meijer <ralphm@ik.nu>
     3# Date 1427898739 -7200
     4#      Wed Apr 01 16:32:19 2015 +0200
     5# Node ID a7e41ac1ecd9b0df220142142eafd92aa07481c4
     6# Parent  56607e5ddb53e3fafbecb41118bd220dea9310c7
     7Add decorator for writing async stanza handlers.
    38
     9diff --git a/wokkel/generic.py b/wokkel/generic.py
     10--- a/wokkel/generic.py
     11+++ b/wokkel/generic.py
     12@@ -7,18 +7,22 @@
     13 Generic XMPP protocol helpers.
     14 """
     15 
     16+__all__ = ['parseXml', 'FallbackHandler', 'VersionHandler', 'XmlPipe',
     17+           'DeferredXmlStreamFactory', 'prepareIDNName',
     18+           'Stanza', 'Request', 'ErrorStanza']
     19+
     20 from zope.interface import implements
     21 
     22 from twisted.internet import defer, protocol
     23-from twisted.python import reflect
     24 from twisted.python.deprecate import deprecated
     25 from twisted.python.versions import Version
     26-from twisted.words.protocols.jabber import error, jid, xmlstream
     27+from twisted.words.protocols.jabber import error, xmlstream
     28 from twisted.words.protocols.jabber.xmlstream import toResponse
     29 from twisted.words.xish import domish, utility
     30 from twisted.words.xish.xmlstream import BootstrapMixin
     31 
     32 from wokkel.iwokkel import IDisco
     33+from wokkel.stanza import Stanza, ErrorStanza, Request
     34 from wokkel.subprotocols import XMPPHandler
     35 
     36 IQ_GET = '/iq[@type="get"]'
     37@@ -47,24 +51,6 @@
     38 
     39 
     40 
     41-def stripNamespace(rootElement):
     42-    namespace = rootElement.uri
     43-
     44-    def strip(element):
     45-        if element.uri == namespace:
     46-            element.uri = None
     47-            if element.defaultUri == namespace:
     48-                element.defaultUri = None
     49-            for child in element.elements():
     50-                strip(child)
     51-
     52-    if namespace is not None:
     53-        strip(rootElement)
     54-
     55-    return rootElement
     56-
     57-
     58-
     59 class FallbackHandler(XMPPHandler):
     60     """
     61     XMPP subprotocol handler that catches unhandled iq requests.
     62@@ -162,145 +148,6 @@
     63 
     64 
     65 
     66-class Stanza(object):
     67-    """
     68-    Abstract representation of a stanza.
     69-
     70-    @ivar sender: The sending entity.
     71-    @type sender: L{jid.JID}
     72-    @ivar recipient: The receiving entity.
     73-    @type recipient: L{jid.JID}
     74-    """
     75-
     76-    recipient = None
     77-    sender = None
     78-    stanzaKind = None
     79-    stanzaID = None
     80-    stanzaType = None
     81-
     82-    def __init__(self, recipient=None, sender=None):
     83-        self.recipient = recipient
     84-        self.sender = sender
     85-
     86-
     87-    @classmethod
     88-    def fromElement(Class, element):
     89-        """
     90-        Create a stanza from a L{domish.Element}.
     91-        """
     92-        stanza = Class()
     93-        stanza.parseElement(element)
     94-        return stanza
     95-
     96-
     97-    def parseElement(self, element):
     98-        """
     99-        Parse the stanza element.
     100-
     101-        This is called with the stanza's element when a L{Stanza} is
     102-        created using L{fromElement}. It parses the stanza's core attributes
     103-        (addressing, type and id), strips the namespace from the stanza
     104-        element for easier transport across streams and passes on
     105-        child elements for further parsing.
     106-
     107-        Child element parsers are defined by providing a C{childParsers}
     108-        attribute on a subclass, as a mapping from (URI, name) to the name
     109-        of the handler on C{self}. C{parseElement} will accumulate
     110-        C{childParsers} from its class hierarchy, iterate over the child
     111-        elements and pass it to matching handlers based on the child element's
     112-        URI and name. The special key of C{None} can be used to pass all
     113-        child elements to.
     114-        """
     115-        if element.hasAttribute('from'):
     116-            self.sender = jid.internJID(element['from'])
     117-        if element.hasAttribute('to'):
     118-            self.recipient = jid.internJID(element['to'])
     119-        self.stanzaType = element.getAttribute('type')
     120-        self.stanzaID = element.getAttribute('id')
     121-
     122-        # Save element
     123-        stripNamespace(element)
     124-        self.element = element
     125-
     126-        # accumulate all childHandlers in the class hierarchy of Class
     127-        handlers = {}
     128-        reflect.accumulateClassDict(self.__class__, 'childParsers', handlers)
     129-
     130-        for child in element.elements():
     131-            try:
     132-                handler = handlers[child.uri, child.name]
     133-            except KeyError:
     134-                try:
     135-                    handler = handlers[None]
     136-                except KeyError:
     137-                    continue
     138-
     139-            getattr(self, handler)(child)
     140-
     141-
     142-    def toElement(self):
     143-        element = domish.Element((None, self.stanzaKind))
     144-        if self.sender is not None:
     145-            element['from'] = self.sender.full()
     146-        if self.recipient is not None:
     147-            element['to'] = self.recipient.full()
     148-        if self.stanzaType:
     149-            element['type'] = self.stanzaType
     150-        if self.stanzaID:
     151-            element['id'] = self.stanzaID
     152-        return element
     153-
     154-
     155-
     156-class ErrorStanza(Stanza):
     157-
     158-    def parseElement(self, element):
     159-        Stanza.parseElement(self, element)
     160-        self.exception = error.exceptionFromStanza(element)
     161-
     162-
     163-
     164-class Request(Stanza):
     165-    """
     166-    IQ request stanza.
     167-
     168-    This is a base class for IQ get or set stanzas, to be used with
     169-    L{wokkel.subprotocols.StreamManager.request}.
     170-    """
     171-
     172-    stanzaKind = 'iq'
     173-    stanzaType = 'get'
     174-    timeout = None
     175-
     176-    childParsers = {None: 'parseRequest'}
     177-
     178-    def __init__(self, recipient=None, sender=None, stanzaType=None):
     179-        Stanza.__init__(self, recipient=recipient, sender=sender)
     180-        if stanzaType is not None:
     181-            self.stanzaType = stanzaType
     182-
     183-
     184-    def parseRequest(self, element):
     185-        """
     186-        Called with the request's child element for parsing.
     187-
     188-        When a request instance is created using L{fromElement}, this method
     189-        is called with the child element of the iq. Override this method for
     190-        parsing the request's payload.
     191-        """
     192-
     193-
     194-    def toElement(self):
     195-        element = Stanza.toElement(self)
     196-
     197-        if not self.stanzaID:
     198-            element.addUniqueId()
     199-            self.stanzaID = element['id']
     200-
     201-        return element
     202-
     203-
     204-
     205 class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory):
     206     protocol = xmlstream.XmlStream
     207 
     208diff --git a/wokkel/stanza.py b/wokkel/stanza.py
     209new file mode 100644
     210--- /dev/null
     211+++ b/wokkel/stanza.py
     212@@ -0,0 +1,170 @@
     213+# -*- test-case-name: wokkel.test.test_generic -*-
     214+#
     215+# Copyright (c) Ralph Meijer.
     216+# See LICENSE for details.
     217+
     218+"""
     219+XMPP Stanza helpers.
     220+"""
     221+
     222+from twisted.python import reflect
     223+from twisted.words.protocols.jabber import error, jid
     224+from twisted.words.xish import domish
     225+
     226+def stripNamespace(rootElement):
     227+    namespace = rootElement.uri
     228+
     229+    def strip(element):
     230+        if element.uri == namespace:
     231+            element.uri = None
     232+            if element.defaultUri == namespace:
     233+                element.defaultUri = None
     234+            for child in element.elements():
     235+                strip(child)
     236+
     237+    if namespace is not None:
     238+        strip(rootElement)
     239+
     240+    return rootElement
     241+
     242+
     243+
     244+class Stanza(object):
     245+    """
     246+    Abstract representation of a stanza.
     247+
     248+    @ivar sender: The sending entity.
     249+    @type sender: L{jid.JID}
     250+    @ivar recipient: The receiving entity.
     251+    @type recipient: L{jid.JID}
     252+    """
     253+
     254+    recipient = None
     255+    sender = None
     256+    stanzaKind = None
     257+    stanzaID = None
     258+    stanzaType = None
     259+
     260+    def __init__(self, recipient=None, sender=None):
     261+        self.recipient = recipient
     262+        self.sender = sender
     263+
     264+
     265+    @classmethod
     266+    def fromElement(Class, element):
     267+        """
     268+        Create a stanza from a L{domish.Element}.
     269+        """
     270+        stanza = Class()
     271+        stanza.parseElement(element)
     272+        return stanza
     273+
     274+
     275+    def parseElement(self, element):
     276+        """
     277+        Parse the stanza element.
     278+
     279+        This is called with the stanza's element when a L{Stanza} is
     280+        created using L{fromElement}. It parses the stanza's core attributes
     281+        (addressing, type and id), strips the namespace from the stanza
     282+        element for easier transport across streams and passes on
     283+        child elements for further parsing.
     284+
     285+        Child element parsers are defined by providing a C{childParsers}
     286+        attribute on a subclass, as a mapping from (URI, name) to the name
     287+        of the handler on C{self}. C{parseElement} will accumulate
     288+        C{childParsers} from its class hierarchy, iterate over the child
     289+        elements and pass it to matching handlers based on the child element's
     290+        URI and name. The special key of C{None} can be used to pass all
     291+        child elements to.
     292+        """
     293+        if element.hasAttribute('from'):
     294+            self.sender = jid.internJID(element['from'])
     295+        if element.hasAttribute('to'):
     296+            self.recipient = jid.internJID(element['to'])
     297+        self.stanzaType = element.getAttribute('type')
     298+        self.stanzaID = element.getAttribute('id')
     299+
     300+        # Save element
     301+        stripNamespace(element)
     302+        self.element = element
     303+
     304+        # accumulate all childHandlers in the class hierarchy of Class
     305+        handlers = {}
     306+        reflect.accumulateClassDict(self.__class__, 'childParsers', handlers)
     307+
     308+        for child in element.elements():
     309+            try:
     310+                handler = handlers[child.uri, child.name]
     311+            except KeyError:
     312+                try:
     313+                    handler = handlers[None]
     314+                except KeyError:
     315+                    continue
     316+
     317+            getattr(self, handler)(child)
     318+
     319+
     320+    def toElement(self):
     321+        element = domish.Element((None, self.stanzaKind))
     322+        if self.sender is not None:
     323+            element['from'] = self.sender.full()
     324+        if self.recipient is not None:
     325+            element['to'] = self.recipient.full()
     326+        if self.stanzaType:
     327+            element['type'] = self.stanzaType
     328+        if self.stanzaID:
     329+            element['id'] = self.stanzaID
     330+        return element
     331+
     332+
     333+
     334+class ErrorStanza(Stanza):
     335+
     336+    def parseElement(self, element):
     337+        Stanza.parseElement(self, element)
     338+        self.exception = error.exceptionFromStanza(element)
     339+
     340+
     341+
     342+class Request(Stanza):
     343+    """
     344+    IQ request stanza.
     345+
     346+    This is a base class for IQ get or set stanzas, to be used with
     347+    L{wokkel.subprotocols.StreamManager.request}.
     348+    """
     349+
     350+    stanzaKind = 'iq'
     351+    stanzaType = 'get'
     352+    timeout = None
     353+
     354+    childParsers = {None: 'parseRequest'}
     355+
     356+    def __init__(self, recipient=None, sender=None, stanzaType=None):
     357+        Stanza.__init__(self, recipient=recipient, sender=sender)
     358+        if stanzaType is not None:
     359+            self.stanzaType = stanzaType
     360+
     361+
     362+    def parseRequest(self, element):
     363+        """
     364+        Called with the request's child element for parsing.
     365+
     366+        When a request instance is created using L{fromElement}, this method
     367+        is called with the child element of the iq. Override this method for
     368+        parsing the request's payload.
     369+        """
     370+
     371+
     372+    def toElement(self):
     373+        element = Stanza.toElement(self)
     374+
     375+        if not self.stanzaID:
     376+            element.addUniqueId()
     377+            self.stanzaID = element['id']
     378+
     379+        return element
     380+
     381+
     382+
    4383diff --git a/wokkel/subprotocols.py b/wokkel/subprotocols.py
    5384--- a/wokkel/subprotocols.py
     
    14393 
    15394 from twisted.internet import defer
    16 @@ -379,6 +381,89 @@
     395@@ -23,6 +25,8 @@
     396 from twisted.words.xish import xpath
     397 from twisted.words.xish.domish import IElement
     398 
     399+from wokkel.stanza import Stanza
     400+
     401 deprecatedModuleAttribute(
     402         Version("Wokkel", 0, 7, 0),
     403         "Use twisted.words.protocols.jabber.xmlstream.XMPPHandlerCollection "
     404@@ -285,14 +289,15 @@
     405         """
     406         Handle iq response by firing associated deferred.
     407         """
     408+        stanza = Stanza.fromElement(iq)
     409         try:
     410-            d = self._iqDeferreds[iq["id"]]
     411+            d = self._iqDeferreds[(stanza.sender, stanza.stanzaID)]
     412         except KeyError:
     413             return
     414 
     415-        del self._iqDeferreds[iq["id"]]
     416+        del self._iqDeferreds[(stanza.sender, stanza.stanzaID)]
     417         iq.handled = True
     418-        if iq['type'] == 'error':
     419+        if stanza.stanzaType == 'error':
     420             d.errback(error.exceptionFromStanza(iq))
     421         else:
     422             d.callback(iq)
     423@@ -356,13 +361,13 @@
     424 
     425         # Set up iq response tracking
     426         d = defer.Deferred()
     427-        self._iqDeferreds[element['id']] = d
     428+        self._iqDeferreds[(request.recipient, request.stanzaID)] = d
     429 
     430         timeout = getattr(request, 'timeout', self.timeout)
     431 
     432         if timeout is not None:
     433             def onTimeout():
     434-                del self._iqDeferreds[element['id']]
     435+                del self._iqDeferreds[(request.recipient, request.stanzaID)]
     436                 d.errback(xmlstream.TimeoutError("IQ timed out"))
     437 
     438             call = self._reactor.callLater(timeout, onTimeout)
     439@@ -379,6 +384,90 @@
    17440 
    18441 
     
    47470+    The return value of the wrapped observer is used to set the C{handled}
    48471+    attribute, so that handlers may choose to ignore processing the same
    49 +    stanza.
     472+    stanza. This is expected to be of type C{boolean} or a deferred. In the
     473+    latter case, the C{handled} attribute is set to C{True}.
    50474+
    51475+    If an exception is raised, or the deferred has its errback called, the
     
    96520+            result.addErrback(log.err)
    97521+
    98 +        element.handled = result
     522+        element.handled = bool(result)
    99523+    return observe
    100524+
     
    104528     """
    105529     XMPP subprotocol mixin for handle incoming IQ stanzas.
    106 @@ -401,15 +486,15 @@
     530@@ -401,15 +490,15 @@
    107531 
    108532     A typical way to use this mixin, is to set up L{xpath} observers on the
     
    124548         ...          "/iq[@type='get' or @type='set']" + QUERY_ROSTER,
    125549         ...          self.handleRequest)
    126 @@ -425,6 +510,7 @@
     550@@ -425,6 +514,7 @@
    127551 
    128552     iqHandlers = None
     
    132556         """
    133557         Find a handler and wrap the call for sending a response stanza.
    134 @@ -435,25 +521,13 @@
     558@@ -435,25 +525,13 @@
    135559             if result:
    136560                 if IElement.providedBy(result):
     
    159583         for queryString, method in self.iqHandlers.iteritems():
    160584             if xpath.internQuery(queryString).matches(iq):
    161 @@ -461,14 +535,10 @@
     585@@ -461,14 +539,10 @@
    162586 
    163587         if handler:
     
    178602+        d.addErrback(trapNotImplemented)
    179603+        return d
     604diff --git a/wokkel/test/test_stanza.py b/wokkel/test/test_stanza.py
     605new file mode 100644
     606--- /dev/null
     607+++ b/wokkel/test/test_stanza.py
     608@@ -0,0 +1,218 @@
     609+# Copyright (c) Ralph Meijer.
     610+# See LICENSE for details.
     611+
     612+from twisted.trial import unittest
     613+from twisted.words.protocols.jabber.jid import JID
     614+
     615+from wokkel import generic
     616+
     617+NS_VERSION = 'jabber:iq:version'
     618+
     619+"""
     620+Tests for L{wokkel.generic}.
     621+"""
     622+
     623+
     624+
     625+class StanzaTest(unittest.TestCase):
     626+    """
     627+    Tests for L{generic.Stanza}.
     628+    """
     629+
     630+    def test_fromElement(self):
     631+        xml = """
     632+        <message type='chat' from='other@example.org' to='user@example.org'/>
     633+        """
     634+
     635+        stanza = generic.Stanza.fromElement(generic.parseXml(xml))
     636+        self.assertEqual('chat', stanza.stanzaType)
     637+        self.assertEqual(JID('other@example.org'), stanza.sender)
     638+        self.assertEqual(JID('user@example.org'), stanza.recipient)
     639+
     640+
     641+    def test_fromElementChildParser(self):
     642+        """
     643+        Child elements for which no parser is defined are ignored.
     644+        """
     645+        xml = """
     646+        <message from='other@example.org' to='user@example.org'>
     647+          <x xmlns='http://example.org/'/>
     648+        </message>
     649+        """
     650+
     651+        class Message(generic.Stanza):
     652+            childParsers = {('http://example.org/', 'x'): '_childParser_x'}
     653+            elements = []
     654+
     655+            def _childParser_x(self, element):
     656+                self.elements.append(element)
     657+
     658+        message = Message.fromElement(generic.parseXml(xml))
     659+        self.assertEqual(1, len(message.elements))
     660+
     661+
     662+    def test_fromElementChildParserAll(self):
     663+        """
     664+        Child elements for which no parser is defined are ignored.
     665+        """
     666+        xml = """
     667+        <message from='other@example.org' to='user@example.org'>
     668+          <x xmlns='http://example.org/'/>
     669+        </message>
     670+        """
     671+
     672+        class Message(generic.Stanza):
     673+            childParsers = {None: '_childParser'}
     674+            elements = []
     675+
     676+            def _childParser(self, element):
     677+                self.elements.append(element)
     678+
     679+        message = Message.fromElement(generic.parseXml(xml))
     680+        self.assertEqual(1, len(message.elements))
     681+
     682+
     683+    def test_fromElementChildParserUnknown(self):
     684+        """
     685+        Child elements for which no parser is defined are ignored.
     686+        """
     687+        xml = """
     688+        <message from='other@example.org' to='user@example.org'>
     689+          <x xmlns='http://example.org/'/>
     690+        </message>
     691+        """
     692+        generic.Stanza.fromElement(generic.parseXml(xml))
     693+
     694+
     695+
     696+
     697+class RequestTest(unittest.TestCase):
     698+    """
     699+    Tests for L{generic.Request}.
     700+    """
     701+
     702+    def setUp(self):
     703+        self.request = generic.Request()
     704+
     705+
     706+    def test_requestParser(self):
     707+        """
     708+        The request's child element is passed to requestParser.
     709+        """
     710+        xml = """
     711+        <iq type='get'>
     712+          <query xmlns='jabber:iq:version'/>
     713+        </iq>
     714+        """
     715+
     716+        class VersionRequest(generic.Request):
     717+            elements = []
     718+
     719+            def parseRequest(self, element):
     720+                self.elements.append((element.uri, element.name))
     721+
     722+        request = VersionRequest.fromElement(generic.parseXml(xml))
     723+        self.assertEqual([(NS_VERSION, 'query')], request.elements)
     724+
     725+
     726+    def test_toElementStanzaKind(self):
     727+        """
     728+        A request is an iq stanza.
     729+        """
     730+        element = self.request.toElement()
     731+        self.assertIdentical(None, element.uri)
     732+        self.assertEquals('iq', element.name)
     733+
     734+
     735+    def test_toElementStanzaType(self):
     736+        """
     737+        The request has type 'get'.
     738+        """
     739+        self.assertEquals('get', self.request.stanzaType)
     740+        element = self.request.toElement()
     741+        self.assertEquals('get', element.getAttribute('type'))
     742+
     743+
     744+    def test_toElementStanzaTypeSet(self):
     745+        """
     746+        The request has type 'set'.
     747+        """
     748+        self.request.stanzaType = 'set'
     749+        element = self.request.toElement()
     750+        self.assertEquals('set', element.getAttribute('type'))
     751+
     752+
     753+    def test_toElementStanzaID(self):
     754+        """
     755+        A request, when rendered, has an identifier.
     756+        """
     757+        element = self.request.toElement()
     758+        self.assertNotIdentical(None, self.request.stanzaID)
     759+        self.assertEquals(self.request.stanzaID, element.getAttribute('id'))
     760+
     761+
     762+    def test_toElementRecipient(self):
     763+        """
     764+        A request without recipient, has no 'to' attribute.
     765+        """
     766+        self.request = generic.Request(recipient=JID('other@example.org'))
     767+        self.assertEquals(JID('other@example.org'), self.request.recipient)
     768+        element = self.request.toElement()
     769+        self.assertEquals(u'other@example.org', element.getAttribute('to'))
     770+
     771+
     772+    def test_toElementRecipientNone(self):
     773+        """
     774+        A request without recipient, has no 'to' attribute.
     775+        """
     776+        element = self.request.toElement()
     777+        self.assertFalse(element.hasAttribute('to'))
     778+
     779+
     780+    def test_toElementSender(self):
     781+        """
     782+        A request with sender, has a 'from' attribute.
     783+        """
     784+        self.request = generic.Request(sender=JID('user@example.org'))
     785+        self.assertEquals(JID('user@example.org'), self.request.sender)
     786+        element = self.request.toElement()
     787+        self.assertEquals(u'user@example.org', element.getAttribute('from'))
     788+
     789+
     790+    def test_toElementSenderNone(self):
     791+        """
     792+        A request without sender, has no 'from' attribute.
     793+        """
     794+        element = self.request.toElement()
     795+        self.assertFalse(element.hasAttribute('from'))
     796+
     797+
     798+    def test_timeoutDefault(self):
     799+        """
     800+        The default is no timeout.
     801+        """
     802+        self.assertIdentical(None, self.request.timeout)
     803+
     804+
     805+    def test_stanzaTypeInit(self):
     806+        """
     807+        If stanzaType is passed in __init__, it overrides the class variable.
     808+        """
     809+
     810+        class SetRequest(generic.Request):
     811+            stanzaType = 'set'
     812+
     813+        request = SetRequest(stanzaType='get')
     814+        self.assertEqual('get', request.stanzaType)
     815+
     816+
     817+    def test_stanzaTypeClass(self):
     818+        """
     819+        If stanzaType is not passed in __init__, the class variable is used.
     820+        """
     821+
     822+        class SetRequest(generic.Request):
     823+            stanzaType = 'set'
     824+
     825+        request = SetRequest()
     826+        self.assertEqual('set', request.stanzaType)
    180827diff --git a/wokkel/test/test_subprotocols.py b/wokkel/test/test_subprotocols.py
    181828--- a/wokkel/test/test_subprotocols.py
    182829+++ b/wokkel/test/test_subprotocols.py
    183 @@ -790,13 +790,175 @@
     830@@ -14,6 +14,7 @@
     831 from twisted.python import failure
     832 from twisted.words.xish import domish
     833 from twisted.words.protocols.jabber import error, ijabber, xmlstream
     834+from twisted.words.protocols.jabber.jid import JID
     835 
     836 from wokkel import generic, subprotocols
     837 
     838@@ -202,7 +203,9 @@
     839         self.transport = proto_helpers.StringTransport()
     840         self.xmlstream.transport = self.transport
     841 
     842-        self.request = IQGetStanza()
     843+        self.request = IQGetStanza(recipient=JID('other@example.org'),
     844+                                   sender=JID('user@example.org'))
     845+
     846 
     847     def _streamStarted(self):
     848         """
     849@@ -562,7 +565,8 @@
     850         self._streamStarted()
     851 
     852         self.streamManager.request(self.request)
     853-        expected = u"<iq type='get' id='%s'/>" % self.request.stanzaID
     854+        expected = (u"<iq to='other@example.org' from='user@example.org'"
     855+                       u" id='%s' type='get'/>" % self.request.stanzaID)
     856         self.assertEquals(expected, self.transport.value())
     857 
     858 
     859@@ -575,7 +579,8 @@
     860         self.request.stanzaID = None
     861         self.streamManager.request(self.request)
     862         self.assertNotIdentical(None, self.request.stanzaID)
     863-        expected = u"<iq type='get' id='%s'/>" % self.request.stanzaID
     864+        expected = (u"<iq to='other@example.org' from='user@example.org'"
     865+                       u" id='%s' type='get'/>" % self.request.stanzaID)
     866         self.assertEquals(expected, self.transport.value())
     867 
     868 
     869@@ -587,7 +592,8 @@
     870         self.streamManager.addHandler(handler)
     871 
     872         self.streamManager.request(self.request)
     873-        expected = u"<iq type='get' id='test'/>"
     874+        expected = (u"<iq to='other@example.org' from='user@example.org'"
     875+                       u" id='test' type='get'/>")
     876 
     877         xs = self.xmlstream
     878         self.assertEquals("", xs.transport.value())
     879@@ -616,7 +622,8 @@
     880         d.addCallback(cb)
     881 
     882         xs = self.xmlstream
     883-        xs.dataReceived("<iq type='result' id='test'/>")
     884+        xs.dataReceived("<iq from='other@example.org' "
     885+                            "type='result' id='test'/>")
     886         return d
     887 
     888 
     889@@ -629,7 +636,8 @@
     890         self.assertFailure(d, error.StanzaError)
     891 
     892         xs = self.xmlstream
     893-        xs.dataReceived("<iq type='error' id='test'/>")
     894+        xs.dataReceived("<iq from='other@example.org' "
     895+                            "type='error' id='test'/>")
     896         return d
     897 
     898 
     899@@ -656,6 +664,40 @@
     900         self.assertFalse(getattr(dispatched[-1], 'handled', False))
     901 
     902 
     903+    def test_requestUnrelatedSenderResponse(self):
     904+        """
     905+        Responses with same id, but different sender are ignored.
     906+
     907+        As stanza identifiers are unique per entity, iq responses must be
     908+        tracked by both the stanza ID and the entity the request was sent to.
     909+        If a response with the same id as the request is received, but from
     910+        a different entity, it is to be ignored.
     911+        """
     912+        # Set up a fallback handler that checks the stanza's handled attribute.
     913+        # If that is set to True, the iq tracker claims to have handled the
     914+        # response.
     915+        dispatched = []
     916+        def cb(iq):
     917+            dispatched.append(iq)
     918+
     919+        self._streamStarted()
     920+        self.xmlstream.addObserver("/iq", cb, -1)
     921+
     922+        d = self.streamManager.request(self.request)
     923+
     924+        # Receive an untracked iq response
     925+        self.xmlstream.dataReceived("<iq from='foo@example.org' "
     926+                                        "type='result' id='test'/>")
     927+        self.assertEquals(1, len(dispatched))
     928+        self.assertFalse(getattr(dispatched[-1], 'handled', False))
     929+
     930+        # Receive expected response
     931+        self.xmlstream.dataReceived("<iq from='other@example.org' "
     932+                                        "type='result' id='test'/>")
     933+
     934+        return d
     935+
     936+
     937     def test_requestCleanup(self):
     938         """
     939         Test if the deferred associated with an iq request is removed
     940@@ -665,7 +707,8 @@
     941         self._streamStarted()
     942         d = self.streamManager.request(self.request)
     943         xs = self.xmlstream
     944-        xs.dataReceived("<iq type='result' id='test'/>")
     945+        xs.dataReceived("<iq from='other@example.org' "
     946+                            "type='result' id='test'/>")
     947         self.assertNotIn('test', self.streamManager._iqDeferreds)
     948         return d
     949 
     950@@ -721,7 +764,8 @@
     951         self.request.timeout = 60
     952         d = self.streamManager.request(self.request)
     953         self.clock.callLater(1, self.xmlstream.dataReceived,
     954-                             "<iq type='result' id='test'/>")
     955+                             "<iq from='other@example.org' "
     956+                                 "type='result' id='test'/>")
     957         self.clock.pump([1, 1])
     958         self.assertFalse(self.clock.calls)
     959         return d
     960@@ -790,13 +834,205 @@
    184961         self.xmlstream.send(obj)
    185962 
     
    2461023+        self.assertTrue(element.handled)
    2471024+        self.assertFalse(self.output)
     1025+
     1026+
     1027+    def test_syncTruthy(self):
     1028+        """
     1029+        A truthy value results in element marked as handled.
     1030+        """
     1031+        @subprotocols.asyncObserver
     1032+        def observer(self, element):
     1033+            return 3
     1034+
     1035+        element = domish.Element((None, u'message'))
     1036+
     1037+        observer(self, element)
     1038+        self.assertTrue(element.handled)
     1039+        self.assertIsInstance(element.handled, bool)
     1040+
     1041+
     1042+    def test_syncNonTruthy(self):
     1043+        """
     1044+        A non-truthy value results in element marked as not handled.
     1045+        """
     1046+        @subprotocols.asyncObserver
     1047+        def observer(self, element):
     1048+            return None
     1049+
     1050+        element = domish.Element((None, u'message'))
     1051+
     1052+        observer(self, element)
     1053+        self.assertFalse(element.handled)
     1054+        self.assertIsInstance(element.handled, bool)
    2481055+
    2491056+
     
    3591166             called = False
    3601167 
    361 @@ -810,11 +972,11 @@
     1168@@ -810,11 +1046,11 @@
    3621169         handler.handleRequest(iq)
    3631170         self.assertTrue(handler.called)
     
    3731180             called = False
    3741181 
    375 @@ -828,11 +990,11 @@
     1182@@ -828,11 +1064,11 @@
    3761183         handler.handleRequest(iq)
    3771184         self.assertFalse(handler.called)
     
    3871194             def onGet(self, iq):
    3881195                 return None
    389 @@ -847,11 +1009,11 @@
     1196@@ -847,11 +1083,11 @@
    3901197         self.assertEquals('iq', response.name)
    3911198         self.assertEquals('result', response['type'])
     
    4011208             payload = domish.Element(('testns', 'foo'))
    4021209 
    403 @@ -870,11 +1032,11 @@
     1210@@ -870,11 +1106,11 @@
    4041211         payload = response.elements().next()
    4051212         self.assertEqual(handler.payload, payload)
     
    4151222             def onGet(self, iq):
    4161223                 return defer.succeed(None)
    417 @@ -889,11 +1051,11 @@
     1224@@ -889,11 +1125,11 @@
    4181225         self.assertEquals('iq', response.name)
    4191226         self.assertEquals('result', response['type'])
     
    4291236             def onGet(self, iq):
    4301237                 raise error.StanzaError('forbidden')
    431 @@ -910,11 +1072,11 @@
     1238@@ -910,11 +1146,11 @@
    4321239         e = error.exceptionFromStanza(response)
    4331240         self.assertEquals('forbidden', e.condition)
     
    4431250             pass
    4441251 
    445 @@ -935,11 +1097,11 @@
     1252@@ -935,11 +1171,11 @@
    4461253         self.assertEquals('internal-server-error', e.condition)
    4471254         self.assertEquals(1, len(self.flushLoggedErrors(TestError)))
     
    4571264             def onGet(self, iq):
    4581265                 raise NotImplementedError()
    459 @@ -956,11 +1118,11 @@
     1266@@ -956,11 +1192,11 @@
    4601267         e = error.exceptionFromStanza(response)
    4611268         self.assertEquals('feature-not-implemented', e.condition)
  • series

    r76 r77  
     1delay-keyerror.patch
    12async-observer.patch
    23message-stanza.patch
Note: See TracChangeset for help on using the changeset viewer.