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@…>, 6 years ago | |
---|---|
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 7 7 Generic XMPP protocol helpers. 8 8 """ 9 9 10 __all__ = ['parseXml', 'FallbackHandler', 'VersionHandler', 'XmlPipe', 11 'DeferredXmlStreamFactory', 'prepareIDNName', 12 'Stanza', 'Request', 'ErrorStanza'] 13 10 14 from zope.interface import implements 11 15 12 16 from twisted.internet import defer, protocol 13 from twisted.python import reflect14 17 from twisted.python.deprecate import deprecated 15 18 from twisted.python.versions import Version 16 from twisted.words.protocols.jabber import error, jid,xmlstream19 from twisted.words.protocols.jabber import error, xmlstream 17 20 from twisted.words.protocols.jabber.xmlstream import toResponse 18 21 from twisted.words.xish import domish, utility 19 22 from twisted.words.xish.xmlstream import BootstrapMixin 20 23 21 24 from wokkel.iwokkel import IDisco 25 from wokkel.stanza import Stanza, ErrorStanza, Request 22 26 from wokkel.subprotocols import XMPPHandler 23 27 24 28 IQ_GET = '/iq[@type="get"]' … … 47 51 48 52 49 53 50 def stripNamespace(rootElement):51 namespace = rootElement.uri52 53 def strip(element):54 if element.uri == namespace:55 element.uri = None56 if element.defaultUri == namespace:57 element.defaultUri = None58 for child in element.elements():59 strip(child)60 61 if namespace is not None:62 strip(rootElement)63 64 return rootElement65 66 67 68 54 class FallbackHandler(XMPPHandler): 69 55 """ 70 56 XMPP subprotocol handler that catches unhandled iq requests. … … 162 148 163 149 164 150 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 = None176 sender = None177 stanzaKind = None178 stanzaID = None179 stanzaType = None180 181 def __init__(self, recipient=None, sender=None):182 self.recipient = recipient183 self.sender = sender184 185 186 @classmethod187 def fromElement(Class, element):188 """189 Create a stanza from a L{domish.Element}.190 """191 stanza = Class()192 stanza.parseElement(element)193 return stanza194 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} is201 created using L{fromElement}. It parses the stanza's core attributes202 (addressing, type and id), strips the namespace from the stanza203 element for easier transport across streams and passes on204 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 name208 of the handler on C{self}. C{parseElement} will accumulate209 C{childParsers} from its class hierarchy, iterate over the child210 elements and pass it to matching handlers based on the child element's211 URI and name. The special key of C{None} can be used to pass all212 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 element222 stripNamespace(element)223 self.element = element224 225 # accumulate all childHandlers in the class hierarchy of Class226 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 continue237 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.stanzaType249 if self.stanzaID:250 element['id'] = self.stanzaID251 return element252 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 with268 L{wokkel.subprotocols.StreamManager.request}.269 """270 271 stanzaKind = 'iq'272 stanzaType = 'get'273 timeout = None274 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 = stanzaType281 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 method288 is called with the child element of the iq. Override this method for289 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 element301 302 303 304 151 class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory): 305 152 protocol = xmlstream.XmlStream 306 153 -
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 """ 7 XMPP Stanza helpers. 8 """ 9 10 from twisted.python import reflect 11 from twisted.words.protocols.jabber import error, jid 12 from twisted.words.xish import domish 13 14 def 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 32 class 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 122 class ErrorStanza(Stanza): 123 124 def parseElement(self, element): 125 Stanza.parseElement(self, element) 126 self.exception = error.exceptionFromStanza(element) 127 128 129 130 class 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 10 10 __all__ = ['XMPPHandler', 'XMPPHandlerCollection', 'StreamManager', 11 11 'IQHandlerMixin'] 12 12 13 from functools import wraps 14 13 15 from zope.interface import implements 14 16 15 17 from twisted.internet import defer … … 23 25 from twisted.words.xish import xpath 24 26 from twisted.words.xish.domish import IElement 25 27 28 from wokkel.stanza import Stanza 29 26 30 deprecatedModuleAttribute( 27 31 Version("Wokkel", 0, 7, 0), 28 32 "Use twisted.words.protocols.jabber.xmlstream.XMPPHandlerCollection " … … 285 289 """ 286 290 Handle iq response by firing associated deferred. 287 291 """ 292 stanza = Stanza.fromElement(iq) 288 293 try: 289 d = self._iqDeferreds[ iq["id"]]294 d = self._iqDeferreds[(stanza.sender, stanza.stanzaID)] 290 295 except KeyError: 291 296 return 292 297 293 del self._iqDeferreds[ iq["id"]]298 del self._iqDeferreds[(stanza.sender, stanza.stanzaID)] 294 299 iq.handled = True 295 if iq['type']== 'error':300 if stanza.stanzaType == 'error': 296 301 d.errback(error.exceptionFromStanza(iq)) 297 302 else: 298 303 d.callback(iq) … … 356 361 357 362 # Set up iq response tracking 358 363 d = defer.Deferred() 359 self._iqDeferreds[ element['id']] = d364 self._iqDeferreds[(request.recipient, request.stanzaID)] = d 360 365 361 366 timeout = getattr(request, 'timeout', self.timeout) 362 367 363 368 if timeout is not None: 364 369 def onTimeout(): 365 del self._iqDeferreds[ element['id']]370 del self._iqDeferreds[(request.recipient, request.stanzaID)] 366 371 d.errback(xmlstream.TimeoutError("IQ timed out")) 367 372 368 373 call = self._reactor.callLater(timeout, onTimeout) … … 379 384 380 385 381 386 387 def 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 382 471 class IQHandlerMixin(object): 383 472 """ 384 473 XMPP subprotocol mixin for handle incoming IQ stanzas. … … 401 490 402 491 A typical way to use this mixin, is to set up L{xpath} observers on the 403 492 C{xmlstream} to call handleRequest, for example in an overridden 404 L{XMPPHandler.connection Made}. It is likely a good idea to only listen for405 incoming iq get and/org iq set requests, and not for any iq, to prevent406 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: 407 496 408 497 >>> QUERY_ROSTER = "/query[@xmlns='jabber:iq:roster']" 409 498 >>> class MyHandler(XMPPHandler, IQHandlerMixin): 410 499 ... iqHandlers = {"/iq[@type='get']" + QUERY_ROSTER: 'onRosterGet', 411 500 ... "/iq[@type='set']" + QUERY_ROSTER: 'onRosterSet'} 412 ... def connection Made(self):501 ... def connectionInitialized(self): 413 502 ... self.xmlstream.addObserver( 414 503 ... "/iq[@type='get' or @type='set']" + QUERY_ROSTER, 415 504 ... self.handleRequest) … … 425 514 426 515 iqHandlers = None 427 516 517 @asyncObserver 428 518 def handleRequest(self, iq): 429 519 """ 430 520 Find a handler and wrap the call for sending a response stanza. … … 435 525 if result: 436 526 if IElement.providedBy(result): 437 527 response.addChild(result) 438 else:439 for element in result:440 response.addChild(element)441 528 442 529 return response 443 530 444 def checkNotImplemented(failure):531 def trapNotImplemented(failure): 445 532 failure.trap(NotImplementedError) 446 533 raise error.StanzaError('feature-not-implemented') 447 534 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 457 535 handler = None 458 536 for queryString, method in self.iqHandlers.iteritems(): 459 537 if xpath.internQuery(queryString).matches(iq): … … 461 539 462 540 if handler: 463 541 d = defer.maybeDeferred(handler, iq) 542 d.addCallback(toResult, iq) 543 d.addCallback(self.send) 464 544 else: 465 545 d = defer.fail(NotImplementedError()) 466 546 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 4 from twisted.trial import unittest 5 from twisted.words.protocols.jabber.jid import JID 6 7 from wokkel import generic 8 9 NS_VERSION = 'jabber:iq:version' 10 11 """ 12 Tests for L{wokkel.generic}. 13 """ 14 15 16 17 class 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 89 class 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 14 14 from twisted.python import failure 15 15 from twisted.words.xish import domish 16 16 from twisted.words.protocols.jabber import error, ijabber, xmlstream 17 from twisted.words.protocols.jabber.jid import JID 17 18 18 19 from wokkel import generic, subprotocols 19 20 … … 202 203 self.transport = proto_helpers.StringTransport() 203 204 self.xmlstream.transport = self.transport 204 205 205 self.request = IQGetStanza() 206 self.request = IQGetStanza(recipient=JID('other@example.org'), 207 sender=JID('user@example.org')) 208 206 209 207 210 def _streamStarted(self): 208 211 """ … … 562 565 self._streamStarted() 563 566 564 567 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) 566 570 self.assertEquals(expected, self.transport.value()) 567 571 568 572 … … 575 579 self.request.stanzaID = None 576 580 self.streamManager.request(self.request) 577 581 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) 579 584 self.assertEquals(expected, self.transport.value()) 580 585 581 586 … … 587 592 self.streamManager.addHandler(handler) 588 593 589 594 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'/>") 591 597 592 598 xs = self.xmlstream 593 599 self.assertEquals("", xs.transport.value()) … … 616 622 d.addCallback(cb) 617 623 618 624 xs = self.xmlstream 619 xs.dataReceived("<iq type='result' id='test'/>") 625 xs.dataReceived("<iq from='other@example.org' " 626 "type='result' id='test'/>") 620 627 return d 621 628 622 629 … … 629 636 self.assertFailure(d, error.StanzaError) 630 637 631 638 xs = self.xmlstream 632 xs.dataReceived("<iq type='error' id='test'/>") 639 xs.dataReceived("<iq from='other@example.org' " 640 "type='error' id='test'/>") 633 641 return d 634 642 635 643 … … 656 664 self.assertFalse(getattr(dispatched[-1], 'handled', False)) 657 665 658 666 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 659 701 def test_requestCleanup(self): 660 702 """ 661 703 Test if the deferred associated with an iq request is removed … … 665 707 self._streamStarted() 666 708 d = self.streamManager.request(self.request) 667 709 xs = self.xmlstream 668 xs.dataReceived("<iq type='result' id='test'/>") 710 xs.dataReceived("<iq from='other@example.org' " 711 "type='result' id='test'/>") 669 712 self.assertNotIn('test', self.streamManager._iqDeferreds) 670 713 return d 671 714 … … 721 764 self.request.timeout = 60 722 765 d = self.streamManager.request(self.request) 723 766 self.clock.callLater(1, self.xmlstream.dataReceived, 724 "<iq type='result' id='test'/>") 767 "<iq from='other@example.org' " 768 "type='result' id='test'/>") 725 769 self.clock.pump([1, 1]) 726 770 self.assertFalse(self.clock.calls) 727 771 return d … … 790 834 self.xmlstream.send(obj) 791 835 792 836 837 838 class 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 793 1027 class IQHandlerTest(unittest.TestCase): 1028 """ 1029 Tests for L{subprotocols.IQHandler}. 1030 """ 794 1031 795 1032 def test_match(self): 796 1033 """ 797 T est that the matching handler gets called.1034 The matching handler gets called. 798 1035 """ 799 800 1036 class Handler(DummyIQHandler): 801 1037 called = False 802 1038 … … 810 1046 handler.handleRequest(iq) 811 1047 self.assertTrue(handler.called) 812 1048 1049 813 1050 def test_noMatch(self): 814 1051 """ 815 Test that the matching handler getscalled.1052 If the element does not match the handler is not called. 816 1053 """ 817 818 1054 class Handler(DummyIQHandler): 819 1055 called = False 820 1056 … … 828 1064 handler.handleRequest(iq) 829 1065 self.assertFalse(handler.called) 830 1066 1067 831 1068 def test_success(self): 832 1069 """ 833 Test response when the request is handled successfully.1070 If None is returned, an empty result iq is returned. 834 1071 """ 835 836 1072 class Handler(DummyIQHandler): 837 1073 def onGet(self, iq): 838 1074 return None … … 847 1083 self.assertEquals('iq', response.name) 848 1084 self.assertEquals('result', response['type']) 849 1085 1086 850 1087 def test_successPayload(self): 851 1088 """ 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. 853 1090 """ 854 855 1091 class Handler(DummyIQHandler): 856 1092 payload = domish.Element(('testns', 'foo')) 857 1093 … … 870 1106 payload = response.elements().next() 871 1107 self.assertEqual(handler.payload, payload) 872 1108 1109 873 1110 def test_successDeferred(self): 874 1111 """ 875 Test response when where the handler was a deferred.1112 A deferred result is used when fired. 876 1113 """ 877 878 1114 class Handler(DummyIQHandler): 879 1115 def onGet(self, iq): 880 1116 return defer.succeed(None) … … 889 1125 self.assertEquals('iq', response.name) 890 1126 self.assertEquals('result', response['type']) 891 1127 1128 892 1129 def test_failure(self): 893 1130 """ 894 Test response when the request is handled unsuccessfully.1131 A raised StanzaError causes an error response. 895 1132 """ 896 897 1133 class Handler(DummyIQHandler): 898 1134 def onGet(self, iq): 899 1135 raise error.StanzaError('forbidden') … … 910 1146 e = error.exceptionFromStanza(response) 911 1147 self.assertEquals('forbidden', e.condition) 912 1148 1149 913 1150 def test_failureUnknown(self): 914 1151 """ 915 Test response when the request handler raises a non-stanza-error.1152 Any Exception cause an internal-server-error response. 916 1153 """ 917 918 1154 class TestError(Exception): 919 1155 pass 920 1156 … … 935 1171 self.assertEquals('internal-server-error', e.condition) 936 1172 self.assertEquals(1, len(self.flushLoggedErrors(TestError))) 937 1173 1174 938 1175 def test_notImplemented(self): 939 1176 """ 940 Test response when the request is recognised but not implemented.1177 A NotImplementedError causes a feature-not-implemented response. 941 1178 """ 942 943 1179 class Handler(DummyIQHandler): 944 1180 def onGet(self, iq): 945 1181 raise NotImplementedError() … … 956 1192 e = error.exceptionFromStanza(response) 957 1193 self.assertEquals('feature-not-implemented', e.condition) 958 1194 1195 959 1196 def test_noHandler(self): 960 1197 """ 961 Test when the request is not recognised.1198 A missing handler causes a feature-not-implemented response. 962 1199 """ 963 964 1200 iq = domish.Element((None, 'iq')) 965 1201 iq['type'] = 'set' 966 1202 iq['id'] = 'r1'
Note: See TracBrowser
for help on using the repository browser.