Changeset 77:8715d37c78f8 in ralphm-patches
- Timestamp:
- Jun 22, 2016, 9:15:55 AM (6 years ago)
- Branch:
- default
- Files:
-
- 1 added
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
async-observer.patch
r76 r77 1 1 # 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 7 Add decorator for writing async stanza handlers. 3 8 9 diff --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 208 diff --git a/wokkel/stanza.py b/wokkel/stanza.py 209 new 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 + 4 383 diff --git a/wokkel/subprotocols.py b/wokkel/subprotocols.py 5 384 --- a/wokkel/subprotocols.py … … 14 393 15 394 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 @@ 17 440 18 441 … … 47 470 + The return value of the wrapped observer is used to set the C{handled} 48 471 + 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}. 50 474 + 51 475 + If an exception is raised, or the deferred has its errback called, the … … 96 520 + result.addErrback(log.err) 97 521 + 98 + element.handled = result522 + element.handled = bool(result) 99 523 + return observe 100 524 + … … 104 528 """ 105 529 XMPP subprotocol mixin for handle incoming IQ stanzas. 106 @@ -401,15 +4 86,15 @@530 @@ -401,15 +490,15 @@ 107 531 108 532 A typical way to use this mixin, is to set up L{xpath} observers on the … … 124 548 ... "/iq[@type='get' or @type='set']" + QUERY_ROSTER, 125 549 ... self.handleRequest) 126 @@ -425,6 +51 0,7 @@550 @@ -425,6 +514,7 @@ 127 551 128 552 iqHandlers = None … … 132 556 """ 133 557 Find a handler and wrap the call for sending a response stanza. 134 @@ -435,25 +52 1,13 @@558 @@ -435,25 +525,13 @@ 135 559 if result: 136 560 if IElement.providedBy(result): … … 159 583 for queryString, method in self.iqHandlers.iteritems(): 160 584 if xpath.internQuery(queryString).matches(iq): 161 @@ -461,14 +53 5,10 @@585 @@ -461,14 +539,10 @@ 162 586 163 587 if handler: … … 178 602 + d.addErrback(trapNotImplemented) 179 603 + return d 604 diff --git a/wokkel/test/test_stanza.py b/wokkel/test/test_stanza.py 605 new 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) 180 827 diff --git a/wokkel/test/test_subprotocols.py b/wokkel/test/test_subprotocols.py 181 828 --- a/wokkel/test/test_subprotocols.py 182 829 +++ 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 @@ 184 961 self.xmlstream.send(obj) 185 962 … … 246 1023 + self.assertTrue(element.handled) 247 1024 + 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) 248 1055 + 249 1056 + … … 359 1166 called = False 360 1167 361 @@ -810,11 + 972,11 @@1168 @@ -810,11 +1046,11 @@ 362 1169 handler.handleRequest(iq) 363 1170 self.assertTrue(handler.called) … … 373 1180 called = False 374 1181 375 @@ -828,11 + 990,11 @@1182 @@ -828,11 +1064,11 @@ 376 1183 handler.handleRequest(iq) 377 1184 self.assertFalse(handler.called) … … 387 1194 def onGet(self, iq): 388 1195 return None 389 @@ -847,11 +10 09,11 @@1196 @@ -847,11 +1083,11 @@ 390 1197 self.assertEquals('iq', response.name) 391 1198 self.assertEquals('result', response['type']) … … 401 1208 payload = domish.Element(('testns', 'foo')) 402 1209 403 @@ -870,11 +1 032,11 @@1210 @@ -870,11 +1106,11 @@ 404 1211 payload = response.elements().next() 405 1212 self.assertEqual(handler.payload, payload) … … 415 1222 def onGet(self, iq): 416 1223 return defer.succeed(None) 417 @@ -889,11 +1 051,11 @@1224 @@ -889,11 +1125,11 @@ 418 1225 self.assertEquals('iq', response.name) 419 1226 self.assertEquals('result', response['type']) … … 429 1236 def onGet(self, iq): 430 1237 raise error.StanzaError('forbidden') 431 @@ -910,11 +1 072,11 @@1238 @@ -910,11 +1146,11 @@ 432 1239 e = error.exceptionFromStanza(response) 433 1240 self.assertEquals('forbidden', e.condition) … … 443 1250 pass 444 1251 445 @@ -935,11 +1 097,11 @@1252 @@ -935,11 +1171,11 @@ 446 1253 self.assertEquals('internal-server-error', e.condition) 447 1254 self.assertEquals(1, len(self.flushLoggedErrors(TestError))) … … 457 1264 def onGet(self, iq): 458 1265 raise NotImplementedError() 459 @@ -956,11 +11 18,11 @@1266 @@ -956,11 +1192,11 @@ 460 1267 e = error.exceptionFromStanza(response) 461 1268 self.assertEquals('feature-not-implemented', e.condition) -
series
r76 r77 1 delay-keyerror.patch 1 2 async-observer.patch 2 3 message-stanza.patch
Note: See TracChangeset
for help on using the changeset viewer.