source: wokkel/test/test_subprotocols.py @ 99:2c8dc93fbef4

Last change on this file since 99:2c8dc93fbef4 was 99:2c8dc93fbef4, checked in by Ralph Meijer <ralphm@…>, 10 years ago

Fix race condition and nesting errors when adding a new subprotocol handler.

This fixes two related issues with adding a new subprotocol handler:

  • Adding a handler when the stream is not yet initialized (authenticated) does not cause connectionMade to be called.
  • Adding a handler in connectionMade, connectionInitialized, or connectionLost modifies the lists of handlers iterated over, causing some methods being called too often.

Author: ff, kandaurovoleg, ralphm.
Fixes: #48.

  • Property exe set to *
File size: 21.8 KB
Line 
1# Copyright (c) Ralph Meijer.
2# See LICENSE for details.
3
4"""
5Tests for L{wokkel.subprotocols}
6"""
7
8from zope.interface.verify import verifyObject
9
10from twisted.trial import unittest
11from twisted.test import proto_helpers
12from twisted.internet import defer
13from twisted.python import failure
14from twisted.words.xish import domish
15from twisted.words.protocols.jabber import error, xmlstream
16
17from wokkel import iwokkel, subprotocols
18
19class DummyFactory(object):
20    """
21    Dummy XmlStream factory that only registers bootstrap observers.
22    """
23    def __init__(self):
24        self.callbacks = {}
25
26
27    def addBootstrap(self, event, callback):
28        self.callbacks[event] = callback
29
30
31
32class DummyXMPPHandler(subprotocols.XMPPHandler):
33    """
34    Dummy XMPP subprotocol handler to count the methods are called on it.
35    """
36    def __init__(self):
37        self.doneMade = 0
38        self.doneInitialized = 0
39        self.doneLost = 0
40
41
42    def makeConnection(self, xs):
43        self.connectionMade()
44
45
46    def connectionMade(self):
47        self.doneMade += 1
48
49
50    def connectionInitialized(self):
51        self.doneInitialized += 1
52
53
54    def connectionLost(self, reason):
55        self.doneLost += 1
56
57
58
59class FailureReasonXMPPHandler(subprotocols.XMPPHandler):
60    """
61    Dummy handler specifically for failure Reason tests.
62    """
63    def __init__(self):
64        self.gotFailureReason = False
65
66
67    def connectionLost(self, reason):
68        if isinstance(reason, failure.Failure):
69            self.gotFailureReason = True
70
71
72
73class XMPPHandlerTest(unittest.TestCase):
74    """
75    Tests for L{subprotocols.XMPPHandler}.
76    """
77
78    def test_interface(self):
79        """
80        L{xmlstream.XMPPHandler} implements L{iwokkel.IXMPPHandler}.
81        """
82        verifyObject(iwokkel.IXMPPHandler, subprotocols.XMPPHandler())
83
84
85    def test_send(self):
86        """
87        Test that data is passed on for sending by the stream manager.
88        """
89        class DummyStreamManager(object):
90            def __init__(self):
91                self.outlist = []
92
93            def send(self, data):
94                self.outlist.append(data)
95
96        handler = subprotocols.XMPPHandler()
97        handler.parent = DummyStreamManager()
98        handler.send('<presence/>')
99        self.assertEquals(['<presence/>'], handler.parent.outlist)
100
101
102    def test_makeConnection(self):
103        """
104        Test that makeConnection saves the XML stream and calls connectionMade.
105        """
106        class TestXMPPHandler(subprotocols.XMPPHandler):
107            def connectionMade(self):
108                self.doneMade = True
109
110        handler = TestXMPPHandler()
111        xs = xmlstream.XmlStream(xmlstream.Authenticator())
112        handler.makeConnection(xs)
113        self.assertTrue(handler.doneMade)
114        self.assertIdentical(xs, handler.xmlstream)
115
116
117    def test_connectionLost(self):
118        """
119        Test that connectionLost forgets the XML stream.
120        """
121        handler = subprotocols.XMPPHandler()
122        xs = xmlstream.XmlStream(xmlstream.Authenticator())
123        handler.makeConnection(xs)
124        handler.connectionLost(Exception())
125        self.assertIdentical(None, handler.xmlstream)
126
127
128
129class XMPPHandlerCollectionTest(unittest.TestCase):
130    """
131    Tests for L{subprotocols.XMPPHandlerCollection}.
132    """
133
134    def setUp(self):
135        self.collection = subprotocols.XMPPHandlerCollection()
136
137
138    def test_interface(self):
139        """
140        L{subprotocols.StreamManager} implements L{iwokkel.IXMPPHandlerCollection}.
141        """
142        verifyObject(iwokkel.IXMPPHandlerCollection, self.collection)
143
144
145    def test_addHandler(self):
146        """
147        Test the addition of a protocol handler.
148        """
149        handler = DummyXMPPHandler()
150        handler.setHandlerParent(self.collection)
151        self.assertIn(handler, self.collection)
152        self.assertIdentical(self.collection, handler.parent)
153
154
155    def test_removeHandler(self):
156        """
157        Test removal of a protocol handler.
158        """
159        handler = DummyXMPPHandler()
160        handler.setHandlerParent(self.collection)
161        handler.disownHandlerParent(self.collection)
162        self.assertNotIn(handler, self.collection)
163        self.assertIdentical(None, handler.parent)
164
165
166
167class StreamManagerTest(unittest.TestCase):
168    """
169    Tests for L{subprotocols.StreamManager}.
170    """
171
172    def setUp(self):
173        factory = DummyFactory()
174        self.streamManager = subprotocols.StreamManager(factory)
175
176    def test_basic(self):
177        """
178        Test correct initialization and setup of factory observers.
179        """
180        sm = self.streamManager
181        self.assertIdentical(None, sm.xmlstream)
182        self.assertEquals([], sm.handlers)
183        self.assertEquals(sm._connected,
184                          sm.factory.callbacks['//event/stream/connected'])
185        self.assertEquals(sm._authd,
186                          sm.factory.callbacks['//event/stream/authd'])
187        self.assertEquals(sm._disconnected,
188                          sm.factory.callbacks['//event/stream/end'])
189        self.assertEquals(sm.initializationFailed,
190                          sm.factory.callbacks['//event/xmpp/initfailed'])
191
192
193    def test_connected(self):
194        """
195        Test that protocol handlers have their connectionMade method called
196        when the XML stream is connected.
197        """
198        sm = self.streamManager
199        handler = DummyXMPPHandler()
200        handler.setHandlerParent(sm)
201        xs = xmlstream.XmlStream(xmlstream.Authenticator())
202        sm._connected(xs)
203        self.assertEquals(1, handler.doneMade)
204        self.assertEquals(0, handler.doneInitialized)
205        self.assertEquals(0, handler.doneLost)
206
207
208    def test_connectedLogTrafficFalse(self):
209        """
210        Test raw data functions unset when logTraffic is set to False.
211        """
212        sm = self.streamManager
213        handler = DummyXMPPHandler()
214        handler.setHandlerParent(sm)
215        xs = xmlstream.XmlStream(xmlstream.Authenticator())
216        sm._connected(xs)
217        self.assertIdentical(None, xs.rawDataInFn)
218        self.assertIdentical(None, xs.rawDataOutFn)
219
220
221    def test_connectedLogTrafficTrue(self):
222        """
223        Test raw data functions set when logTraffic is set to True.
224        """
225        sm = self.streamManager
226        sm.logTraffic = True
227        handler = DummyXMPPHandler()
228        handler.setHandlerParent(sm)
229        xs = xmlstream.XmlStream(xmlstream.Authenticator())
230        sm._connected(xs)
231        self.assertNotIdentical(None, xs.rawDataInFn)
232        self.assertNotIdentical(None, xs.rawDataOutFn)
233
234
235    def test_authd(self):
236        """
237        Test that protocol handlers have their connectionInitialized method
238        called when the XML stream is initialized.
239        """
240        sm = self.streamManager
241        handler = DummyXMPPHandler()
242        handler.setHandlerParent(sm)
243        xs = xmlstream.XmlStream(xmlstream.Authenticator())
244        sm._authd(xs)
245        self.assertEquals(0, handler.doneMade)
246        self.assertEquals(1, handler.doneInitialized)
247        self.assertEquals(0, handler.doneLost)
248
249
250    def test_disconnected(self):
251        """
252        Protocol handlers have connectionLost called on stream disconnect.
253        """
254        sm = self.streamManager
255        handler = DummyXMPPHandler()
256        handler.setHandlerParent(sm)
257        sm._disconnected(None)
258        self.assertEquals(0, handler.doneMade)
259        self.assertEquals(0, handler.doneInitialized)
260        self.assertEquals(1, handler.doneLost)
261
262
263    def test_disconnectedReason(self):
264        """
265        A L{STREAM_END_EVENT} results in L{StreamManager} firing the handlers
266        L{connectionLost} methods, passing a L{failure.Failure} reason.
267        """
268        sm = self.streamManager
269        handler = FailureReasonXMPPHandler()
270        handler.setHandlerParent(sm)
271        xs = xmlstream.XmlStream(xmlstream.Authenticator())
272        sm._disconnected(failure.Failure(Exception("no reason")))
273        self.assertEquals(True, handler.gotFailureReason)
274
275
276    def test_addHandler(self):
277        """
278        Test the addition of a protocol handler while not connected.
279        """
280        sm = self.streamManager
281        handler = DummyXMPPHandler()
282        handler.setHandlerParent(sm)
283
284        self.assertEquals(0, handler.doneMade)
285        self.assertEquals(0, handler.doneInitialized)
286        self.assertEquals(0, handler.doneLost)
287
288
289    def test_addHandlerConnected(self):
290        """
291        Adding a handler when connected doesn't call connectionInitialized.
292        """
293        sm = self.streamManager
294        xs = xmlstream.XmlStream(xmlstream.Authenticator())
295        sm._connected(xs)
296        handler = DummyXMPPHandler()
297        handler.setHandlerParent(sm)
298
299        self.assertEquals(1, handler.doneMade)
300        self.assertEquals(0, handler.doneInitialized)
301        self.assertEquals(0, handler.doneLost)
302
303
304    def test_addHandlerConnectedNested(self):
305        """
306        Adding a handler in connectionMade doesn't cause 2nd call.
307        """
308        class NestingHandler(DummyXMPPHandler):
309            nestedHandler = None
310
311            def connectionMade(self):
312                DummyXMPPHandler.connectionMade(self)
313                self.nestedHandler = DummyXMPPHandler()
314                self.nestedHandler.setHandlerParent(self.parent)
315
316        sm = self.streamManager
317        xs = xmlstream.XmlStream(xmlstream.Authenticator())
318        handler = NestingHandler()
319        handler.setHandlerParent(sm)
320        sm._connected(xs)
321
322        self.assertEquals(1, handler.doneMade)
323        self.assertEquals(0, handler.doneInitialized)
324        self.assertEquals(0, handler.doneLost)
325
326        self.assertEquals(1, handler.nestedHandler.doneMade)
327        self.assertEquals(0, handler.nestedHandler.doneInitialized)
328        self.assertEquals(0, handler.nestedHandler.doneLost)
329
330
331
332    def test_addHandlerInitialized(self):
333        """
334        Test the addition of a protocol handler after the stream
335        have been initialized.
336
337        Make sure that the handler will have the connected stream
338        passed via C{makeConnection} and have C{connectionInitialized}
339        called.
340        """
341        sm = self.streamManager
342        xs = xmlstream.XmlStream(xmlstream.Authenticator())
343        sm._connected(xs)
344        sm._authd(xs)
345        handler = DummyXMPPHandler()
346        handler.setHandlerParent(sm)
347
348        self.assertEquals(1, handler.doneMade)
349        self.assertEquals(1, handler.doneInitialized)
350        self.assertEquals(0, handler.doneLost)
351
352
353    def test_addHandlerInitializedNested(self):
354        """
355        Adding a handler in connectionInitialized doesn't cause 2nd call.
356        """
357        class NestingHandler(DummyXMPPHandler):
358            nestedHandler = None
359
360            def connectionInitialized(self):
361                DummyXMPPHandler.connectionInitialized(self)
362                self.nestedHandler = DummyXMPPHandler()
363                self.nestedHandler.setHandlerParent(self.parent)
364
365        sm = self.streamManager
366        xs = xmlstream.XmlStream(xmlstream.Authenticator())
367        handler = NestingHandler()
368        handler.setHandlerParent(sm)
369        sm._connected(xs)
370        sm._authd(xs)
371
372        self.assertEquals(1, handler.doneMade)
373        self.assertEquals(1, handler.doneInitialized)
374        self.assertEquals(0, handler.doneLost)
375
376        self.assertEquals(1, handler.nestedHandler.doneMade)
377        self.assertEquals(1, handler.nestedHandler.doneInitialized)
378        self.assertEquals(0, handler.nestedHandler.doneLost)
379
380
381    def test_addHandlerConnectionLostNested(self):
382        """
383        Adding a handler in connectionLost doesn't call connectionLost there.
384        """
385        class NestingHandler(DummyXMPPHandler):
386            nestedHandler = None
387
388            def connectionLost(self, reason):
389                DummyXMPPHandler.connectionLost(self, reason)
390                self.nestedHandler = DummyXMPPHandler()
391                self.nestedHandler.setHandlerParent(self.parent)
392
393        sm = self.streamManager
394        xs = xmlstream.XmlStream(xmlstream.Authenticator())
395        handler = NestingHandler()
396        handler.setHandlerParent(sm)
397        sm._connected(xs)
398        sm._authd(xs)
399        sm._disconnected(xs)
400
401        self.assertEquals(1, handler.doneMade)
402        self.assertEquals(1, handler.doneInitialized)
403        self.assertEquals(1, handler.doneLost)
404
405        self.assertEquals(0, handler.nestedHandler.doneMade)
406        self.assertEquals(0, handler.nestedHandler.doneInitialized)
407        self.assertEquals(0, handler.nestedHandler.doneLost)
408
409
410    def test_removeHandler(self):
411        """
412        Test removal of protocol handler.
413        """
414        sm = self.streamManager
415        handler = DummyXMPPHandler()
416        handler.setHandlerParent(sm)
417        handler.disownHandlerParent(sm)
418        self.assertNotIn(handler, sm)
419        self.assertIdentical(None, handler.parent)
420
421    def test_sendInitialized(self):
422        """
423        Test send when the stream has been initialized.
424
425        The data should be sent directly over the XML stream.
426        """
427        factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator())
428        sm = subprotocols.StreamManager(factory)
429        xs = factory.buildProtocol(None)
430        xs.transport = proto_helpers.StringTransport()
431        xs.connectionMade()
432        xs.dataReceived("<stream:stream xmlns='jabber:client' "
433                        "xmlns:stream='http://etherx.jabber.org/streams' "
434                        "from='example.com' id='12345'>")
435        xs.dispatch(xs, "//event/stream/authd")
436        sm.send("<presence/>")
437        self.assertEquals("<presence/>", xs.transport.value())
438
439
440    def test_sendNotConnected(self):
441        """
442        Test send when there is no established XML stream.
443
444        The data should be cached until an XML stream has been established and
445        initialized.
446        """
447        factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator())
448        sm = subprotocols.StreamManager(factory)
449        handler = DummyXMPPHandler()
450        sm.addHandler(handler)
451
452        xs = factory.buildProtocol(None)
453        xs.transport = proto_helpers.StringTransport()
454        sm.send("<presence/>")
455        self.assertEquals("", xs.transport.value())
456        self.assertEquals("<presence/>", sm._packetQueue[0])
457
458        xs.connectionMade()
459        self.assertEquals("", xs.transport.value())
460        self.assertEquals("<presence/>", sm._packetQueue[0])
461
462        xs.dataReceived("<stream:stream xmlns='jabber:client' "
463                        "xmlns:stream='http://etherx.jabber.org/streams' "
464                        "from='example.com' id='12345'>")
465        xs.dispatch(xs, "//event/stream/authd")
466
467        self.assertEquals("<presence/>", xs.transport.value())
468        self.assertFalse(sm._packetQueue)
469
470
471    def test_sendNotInitialized(self):
472        """
473        Test send when the stream is connected but not yet initialized.
474
475        The data should be cached until the XML stream has been initialized.
476        """
477        factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator())
478        sm = subprotocols.StreamManager(factory)
479        xs = factory.buildProtocol(None)
480        xs.transport = proto_helpers.StringTransport()
481        xs.connectionMade()
482        xs.dataReceived("<stream:stream xmlns='jabber:client' "
483                        "xmlns:stream='http://etherx.jabber.org/streams' "
484                        "from='example.com' id='12345'>")
485        sm.send("<presence/>")
486        self.assertEquals("", xs.transport.value())
487        self.assertEquals("<presence/>", sm._packetQueue[0])
488
489
490    def test_sendDisconnected(self):
491        """
492        Test send after XML stream disconnection.
493
494        The data should be cached until a new XML stream has been established
495        and initialized.
496        """
497        factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator())
498        sm = subprotocols.StreamManager(factory)
499        handler = DummyXMPPHandler()
500        sm.addHandler(handler)
501
502        xs = factory.buildProtocol(None)
503        xs.connectionMade()
504        xs.transport = proto_helpers.StringTransport()
505        xs.connectionLost(None)
506
507        sm.send("<presence/>")
508        self.assertEquals("", xs.transport.value())
509        self.assertEquals("<presence/>", sm._packetQueue[0])
510
511
512
513class DummyIQHandler(subprotocols.IQHandlerMixin):
514    iqHandlers = {'/iq[@type="get"]': 'onGet'}
515
516    def __init__(self):
517        self.output = []
518        self.xmlstream = xmlstream.XmlStream(xmlstream.Authenticator())
519        self.xmlstream.send = self.output.append
520
521    def send(self, obj):
522        self.xmlstream.send(obj)
523
524
525class IQHandlerTest(unittest.TestCase):
526
527    def test_match(self):
528        """
529        Test that the matching handler gets called.
530        """
531
532        class Handler(DummyIQHandler):
533            called = False
534
535            def onGet(self, iq):
536                self.called = True
537
538        iq = domish.Element((None, 'iq'))
539        iq['type'] = 'get'
540        iq['id'] = 'r1'
541        handler = Handler()
542        handler.handleRequest(iq)
543        self.assertTrue(handler.called)
544
545    def test_noMatch(self):
546        """
547        Test that the matching handler gets called.
548        """
549
550        class Handler(DummyIQHandler):
551            called = False
552
553            def onGet(self, iq):
554                self.called = True
555
556        iq = domish.Element((None, 'iq'))
557        iq['type'] = 'set'
558        iq['id'] = 'r1'
559        handler = Handler()
560        handler.handleRequest(iq)
561        self.assertFalse(handler.called)
562
563    def test_success(self):
564        """
565        Test response when the request is handled successfully.
566        """
567
568        class Handler(DummyIQHandler):
569            def onGet(self, iq):
570                return None
571
572        iq = domish.Element((None, 'iq'))
573        iq['type'] = 'get'
574        iq['id'] = 'r1'
575        handler = Handler()
576        handler.handleRequest(iq)
577        response = handler.output[-1]
578        self.assertEquals(None, response.uri)
579        self.assertEquals('iq', response.name)
580        self.assertEquals('result', response['type'])
581
582    def test_successPayload(self):
583        """
584        Test response when the request is handled successfully with payload.
585        """
586
587        class Handler(DummyIQHandler):
588            payload = domish.Element(('testns', 'foo'))
589
590            def onGet(self, iq):
591                return self.payload
592
593        iq = domish.Element((None, 'iq'))
594        iq['type'] = 'get'
595        iq['id'] = 'r1'
596        handler = Handler()
597        handler.handleRequest(iq)
598        response = handler.output[-1]
599        self.assertEquals(None, response.uri)
600        self.assertEquals('iq', response.name)
601        self.assertEquals('result', response['type'])
602        payload = response.elements().next()
603        self.assertEqual(handler.payload, payload)
604
605    def test_successDeferred(self):
606        """
607        Test response when where the handler was a deferred.
608        """
609
610        class Handler(DummyIQHandler):
611            def onGet(self, iq):
612                return defer.succeed(None)
613
614        iq = domish.Element((None, 'iq'))
615        iq['type'] = 'get'
616        iq['id'] = 'r1'
617        handler = Handler()
618        handler.handleRequest(iq)
619        response = handler.output[-1]
620        self.assertEquals(None, response.uri)
621        self.assertEquals('iq', response.name)
622        self.assertEquals('result', response['type'])
623
624    def test_failure(self):
625        """
626        Test response when the request is handled unsuccessfully.
627        """
628
629        class Handler(DummyIQHandler):
630            def onGet(self, iq):
631                raise error.StanzaError('forbidden')
632
633        iq = domish.Element((None, 'iq'))
634        iq['type'] = 'get'
635        iq['id'] = 'r1'
636        handler = Handler()
637        handler.handleRequest(iq)
638        response = handler.output[-1]
639        self.assertEquals(None, response.uri)
640        self.assertEquals('iq', response.name)
641        self.assertEquals('error', response['type'])
642        e = error.exceptionFromStanza(response)
643        self.assertEquals('forbidden', e.condition)
644
645    def test_failureUnknown(self):
646        """
647        Test response when the request handler raises a non-stanza-error.
648        """
649
650        class TestError(Exception):
651            pass
652
653        class Handler(DummyIQHandler):
654            def onGet(self, iq):
655                raise TestError()
656
657        iq = domish.Element((None, 'iq'))
658        iq['type'] = 'get'
659        iq['id'] = 'r1'
660        handler = Handler()
661        handler.handleRequest(iq)
662        response = handler.output[-1]
663        self.assertEquals(None, response.uri)
664        self.assertEquals('iq', response.name)
665        self.assertEquals('error', response['type'])
666        e = error.exceptionFromStanza(response)
667        self.assertEquals('internal-server-error', e.condition)
668        self.assertEquals(1, len(self.flushLoggedErrors(TestError)))
669
670    def test_notImplemented(self):
671        """
672        Test response when the request is recognised but not implemented.
673        """
674
675        class Handler(DummyIQHandler):
676            def onGet(self, iq):
677                raise NotImplementedError()
678
679        iq = domish.Element((None, 'iq'))
680        iq['type'] = 'get'
681        iq['id'] = 'r1'
682        handler = Handler()
683        handler.handleRequest(iq)
684        response = handler.output[-1]
685        self.assertEquals(None, response.uri)
686        self.assertEquals('iq', response.name)
687        self.assertEquals('error', response['type'])
688        e = error.exceptionFromStanza(response)
689        self.assertEquals('feature-not-implemented', e.condition)
690
691    def test_noHandler(self):
692        """
693        Test when the request is not recognised.
694        """
695
696        iq = domish.Element((None, 'iq'))
697        iq['type'] = 'set'
698        iq['id'] = 'r1'
699        handler = DummyIQHandler()
700        handler.handleRequest(iq)
701        response = handler.output[-1]
702        self.assertEquals(None, response.uri)
703        self.assertEquals('iq', response.name)
704        self.assertEquals('error', response['type'])
705        e = error.exceptionFromStanza(response)
706        self.assertEquals('feature-not-implemented', e.condition)
Note: See TracBrowser for help on using the repository browser.