source: wokkel/test/test_component.py @ 169:bb939a909750

Last change on this file since 169:bb939a909750 was 169:bb939a909750, checked in by Ralph Meijer <ralphm@…>, 9 years ago

Let wokkel.component.Component reconnect if first attempt fails.

Author: ralphm.
Fixes #75.

  • Property exe set to *
File size: 16.5 KB
Line 
1# Copyright (c) Ralph Meijer.
2# See LICENSE for details.
3
4"""
5Tests for L{wokkel.component}.
6"""
7
8from zope.interface.verify import verifyObject
9
10from twisted.internet.base import BaseConnector
11from twisted.internet.error import ConnectionRefusedError
12from twisted.internet.task import Clock
13from twisted.python import failure
14from twisted.trial import unittest
15from twisted.words.protocols.jabber import xmlstream
16from twisted.words.protocols.jabber.ijabber import IXMPPHandlerCollection
17from twisted.words.protocols.jabber.jid import JID
18from twisted.words.protocols.jabber.xmlstream import XMPPHandler
19from twisted.words.xish import domish
20
21from wokkel import component
22from wokkel.generic import XmlPipe
23
24class FakeConnector(BaseConnector):
25    """
26    Fake connector that counts connection attempts.
27    """
28    connects = 0
29
30    def connect(self):
31        self.connects += 1
32        BaseConnector.connect(self)
33
34
35    def _makeTransport(self):
36        return None
37
38
39
40class TestableComponent(component.Component):
41    """
42    Testable component.
43
44    This component provides the created factory with a L{Clock}
45    instead of the regular reactor and uses L{FakeConnector} for testing
46    connects and reconnects.
47    """
48
49    def __init__(self, *args, **kwargs):
50        component.Component.__init__(self, *args, **kwargs)
51        self.factory.clock = Clock()
52
53
54    def _getConnection(self):
55        c = FakeConnector(self.factory, None, None)
56        c.connect()
57        return c
58
59
60
61class ComponentTest(unittest.TestCase):
62    """
63    Tests for L{component.Component}.
64    """
65    def test_startServiceReconnectAfterFailure(self):
66        """
67        When the first connection attempt fails, retry.
68        """
69        comp = TestableComponent('example.org', 5347,
70                                 'test.example.org', 'secret')
71
72        # Starting the service initiates a connection attempt.
73        comp.startService()
74        connector = comp._connection
75        self.assertEqual(1, connector.connects)
76
77        # Fail the connection.
78        connector.connectionFailed(ConnectionRefusedError())
79
80        # After a back-off delay, a new connection is attempted.
81        comp.factory.clock.advance(5)
82        self.assertEqual(2, connector.connects)
83
84
85    def test_stopServiceNoReconnect(self):
86        """
87        When the service is stopped, no reconnect is attempted.
88        """
89        comp = TestableComponent('example.org', 5347,
90                                 'test.example.org', 'secret')
91
92        # Starting the service initiates a connection attempt.
93        comp.startService()
94        connector = comp._connection
95
96        # Fail the connection.
97        connector.connectionFailed(ConnectionRefusedError())
98
99        # If the service is stopped before the back-off delay expires,
100        # no new connection is attempted.
101        comp.factory.clock.advance(1)
102        comp.stopService()
103        comp.factory.clock.advance(4)
104        self.assertEqual(1, connector.connects)
105
106
107
108class InternalComponentTest(unittest.TestCase):
109    """
110    Tests for L{component.InternalComponent}.
111    """
112
113    def setUp(self):
114        self.router = component.Router()
115        self.component = component.InternalComponent(self.router, 'component')
116
117
118    def test_interface(self):
119        """
120        L{component.InternalComponent} implements
121        L{IXMPPHandlerCollection}.
122        """
123        verifyObject(IXMPPHandlerCollection, self.component)
124
125
126    def test_startServiceRunning(self):
127        """
128        Starting the service makes it running.
129        """
130        self.assertFalse(self.component.running)
131        self.component.startService()
132        self.assertTrue(self.component.running)
133
134
135    def test_startServiceAddRoute(self):
136        """
137        Starting the service creates a new route.
138        """
139        self.component.startService()
140        self.assertIn('component', self.router.routes)
141
142
143    def test_startServiceNoDomain(self):
144        self.component = component.InternalComponent(self.router)
145        self.component.startService()
146
147
148    def test_startServiceAddMultipleRoutes(self):
149        """
150        Starting the service creates a new route.
151        """
152        self.component.domains.add('component2')
153        self.component.startService()
154        self.assertIn('component', self.router.routes)
155        self.assertIn('component2', self.router.routes)
156
157
158    def test_startServiceHandlerDispatch(self):
159        """
160        Starting the service hooks up handlers.
161        """
162        events = []
163
164        class TestHandler(XMPPHandler):
165
166            def connectionInitialized(self):
167                fn = lambda obj: events.append(obj)
168                self.xmlstream.addObserver('//event/test', fn)
169
170        TestHandler().setHandlerParent(self.component)
171
172        self.component.startService()
173        self.assertEquals([], events)
174        self.component.xmlstream.dispatch(None, '//event/test')
175        self.assertEquals([None], events)
176
177
178    def test_stopServiceNotRunning(self):
179        """
180        Stopping the service makes it not running.
181        """
182        self.component.startService()
183        self.component.stopService()
184        self.assertFalse(self.component.running)
185
186
187    def test_stopServiceRemoveRoute(self):
188        """
189        Stopping the service removes routes.
190        """
191        self.component.startService()
192        self.component.stopService()
193        self.assertNotIn('component', self.router.routes)
194
195
196    def test_stopServiceNoDomain(self):
197        self.component = component.InternalComponent(self.router)
198        self.component.startService()
199        self.component.stopService()
200
201
202    def test_startServiceRemoveMultipleRoutes(self):
203        """
204        Starting the service creates a new route.
205        """
206        self.component.domains.add('component2')
207        self.component.startService()
208        self.component.stopService()
209        self.assertNotIn('component', self.router.routes)
210        self.assertNotIn('component2', self.router.routes)
211
212
213    def test_stopServiceHandlerDispatch(self):
214        """
215        Stopping the service disconnects handlers.
216        """
217        events = []
218
219        class TestHandler(XMPPHandler):
220
221            def connectionLost(self, reason):
222                events.append(reason)
223
224        TestHandler().setHandlerParent(self.component)
225
226        self.component.startService()
227        self.component.stopService()
228        self.assertEquals(1, len(events))
229
230
231    def test_addHandler(self):
232        """
233        Adding a handler connects it to the stream.
234        """
235        events = []
236
237        class TestHandler(XMPPHandler):
238
239            def connectionInitialized(self):
240                fn = lambda obj: events.append(obj)
241                self.xmlstream.addObserver('//event/test', fn)
242
243        self.component.startService()
244        self.component.xmlstream.dispatch(None, '//event/test')
245        self.assertEquals([], events)
246
247        TestHandler().setHandlerParent(self.component)
248        self.component.xmlstream.dispatch(None, '//event/test')
249        self.assertEquals([None], events)
250
251
252    def test_send(self):
253        """
254        A message sent from the component ends up at the router.
255        """
256        events = []
257        fn = lambda obj: events.append(obj)
258        message = domish.Element((None, 'message'))
259
260        self.router.route = fn
261        self.component.startService()
262        self.component.send(message)
263
264        self.assertEquals([message], events)
265
266
267
268class RouterTest(unittest.TestCase):
269    """
270    Tests for L{component.Router}.
271    """
272
273    def test_addRoute(self):
274        """
275        Test route registration and routing on incoming stanzas.
276        """
277        router = component.Router()
278        routed = []
279        router.route = lambda element: routed.append(element)
280
281        pipe = XmlPipe()
282        router.addRoute('example.org', pipe.sink)
283        self.assertEquals(1, len(router.routes))
284        self.assertEquals(pipe.sink, router.routes['example.org'])
285
286        element = domish.Element(('testns', 'test'))
287        pipe.source.send(element)
288        self.assertEquals([element], routed)
289
290
291    def test_route(self):
292        """
293        Test routing of a message.
294        """
295        component1 = XmlPipe()
296        component2 = XmlPipe()
297        router = component.Router()
298        router.addRoute('component1.example.org', component1.sink)
299        router.addRoute('component2.example.org', component2.sink)
300
301        outgoing = []
302        component2.source.addObserver('/*',
303                                      lambda element: outgoing.append(element))
304        stanza = domish.Element((None, 'presence'))
305        stanza['from'] = 'component1.example.org'
306        stanza['to'] = 'component2.example.org'
307        component1.source.send(stanza)
308        self.assertEquals([stanza], outgoing)
309
310
311    def test_routeDefault(self):
312        """
313        Test routing of a message using the default route.
314
315        The default route is the one with C{None} as its key in the
316        routing table. It is taken when there is no more specific route
317        in the routing table that matches the stanza's destination.
318        """
319        component1 = XmlPipe()
320        s2s = XmlPipe()
321        router = component.Router()
322        router.addRoute('component1.example.org', component1.sink)
323        router.addRoute(None, s2s.sink)
324
325        outgoing = []
326        s2s.source.addObserver('/*', lambda element: outgoing.append(element))
327        stanza = domish.Element((None, 'presence'))
328        stanza['from'] = 'component1.example.org'
329        stanza['to'] = 'example.com'
330        component1.source.send(stanza)
331        self.assertEquals([stanza], outgoing)
332
333
334
335class ListenComponentAuthenticatorTest(unittest.TestCase):
336    """
337    Tests for L{component.ListenComponentAuthenticator}.
338    """
339
340    def setUp(self):
341        self.output = []
342        authenticator = component.ListenComponentAuthenticator('secret')
343        self.xmlstream = xmlstream.XmlStream(authenticator)
344        self.xmlstream.send = self.output.append
345
346
347    def loseConnection(self):
348        """
349        Stub loseConnection because we are a transport.
350        """
351        self.xmlstream.connectionLost("no reason")
352
353
354    def test_streamStarted(self):
355        """
356        The received stream header should set several attributes.
357        """
358        observers = []
359
360        def addOnetimeObserver(event, observerfn):
361            observers.append((event, observerfn))
362
363        xs = self.xmlstream
364        xs.addOnetimeObserver = addOnetimeObserver
365
366        xs.makeConnection(self)
367        self.assertIdentical(None, xs.sid)
368        self.assertFalse(xs._headerSent)
369
370        xs.dataReceived("<stream:stream xmlns='jabber:component:accept' "
371                         "xmlns:stream='http://etherx.jabber.org/streams' "
372                         "to='component.example.org'>")
373        self.assertEqual((0, 0), xs.version)
374        self.assertNotIdentical(None, xs.sid)
375        self.assertTrue(xs._headerSent)
376        self.assertEquals(('/*', xs.authenticator.onElement), observers[-1])
377
378
379    def test_streamStartedWrongNamespace(self):
380        """
381        The received stream header should have a correct namespace.
382        """
383        streamErrors = []
384
385        xs = self.xmlstream
386        xs.sendStreamError = streamErrors.append
387        xs.makeConnection(self)
388        xs.dataReceived("<stream:stream xmlns='jabber:client' "
389                         "xmlns:stream='http://etherx.jabber.org/streams' "
390                         "to='component.example.org'>")
391        self.assertEquals(1, len(streamErrors))
392        self.assertEquals('invalid-namespace', streamErrors[-1].condition)
393
394
395    def test_streamStartedNoTo(self):
396        """
397        The received stream header should have a 'to' attribute.
398        """
399        streamErrors = []
400
401        xs = self.xmlstream
402        xs.sendStreamError = streamErrors.append
403        xs.makeConnection(self)
404        xs.dataReceived("<stream:stream xmlns='jabber:component:accept' "
405                         "xmlns:stream='http://etherx.jabber.org/streams'>")
406        self.assertEquals(1, len(streamErrors))
407        self.assertEquals('improper-addressing', streamErrors[-1].condition)
408
409
410    def test_onElement(self):
411        """
412        We expect a handshake element with a hash.
413        """
414        handshakes = []
415
416        xs = self.xmlstream
417        xs.authenticator.onHandshake = handshakes.append
418
419        handshake = domish.Element(('jabber:component:accept', 'handshake'))
420        handshake.addContent('1234')
421        xs.authenticator.onElement(handshake)
422        self.assertEqual('1234', handshakes[-1])
423
424    def test_onElementNotHandshake(self):
425        """
426        Reject elements that are not handshakes
427        """
428        handshakes = []
429        streamErrors = []
430
431        xs = self.xmlstream
432        xs.authenticator.onHandshake = handshakes.append
433        xs.sendStreamError = streamErrors.append
434
435        element = domish.Element(('jabber:component:accept', 'message'))
436        xs.authenticator.onElement(element)
437        self.assertFalse(handshakes)
438        self.assertEquals('not-authorized', streamErrors[-1].condition)
439
440
441    def test_onHandshake(self):
442        """
443        Receiving a handshake matching the secret authenticates the stream.
444        """
445        authd = []
446
447        def authenticated(xs):
448            authd.append(xs)
449
450        xs = self.xmlstream
451        xs.addOnetimeObserver(xmlstream.STREAM_AUTHD_EVENT, authenticated)
452        xs.sid = u'1234'
453        theHash = '32532c0f7dbf1253c095b18b18e36d38d94c1256'
454        xs.authenticator.onHandshake(theHash)
455        self.assertEqual('<handshake/>', self.output[-1])
456        self.assertEquals(1, len(authd))
457
458
459    def test_onHandshakeWrongHash(self):
460        """
461        Receiving a bad handshake should yield a stream error.
462        """
463        streamErrors = []
464        authd = []
465
466        def authenticated(xs):
467            authd.append(xs)
468
469        xs = self.xmlstream
470        xs.addOnetimeObserver(xmlstream.STREAM_AUTHD_EVENT, authenticated)
471        xs.sendStreamError = streamErrors.append
472
473        xs.sid = u'1234'
474        theHash = '1234'
475        xs.authenticator.onHandshake(theHash)
476        self.assertEquals('not-authorized', streamErrors[-1].condition)
477        self.assertEquals(0, len(authd))
478
479
480
481class XMPPComponentServerFactoryTest(unittest.TestCase):
482    """
483    Tests for L{component.XMPPComponentServerFactory}.
484    """
485
486    def setUp(self):
487        self.router = component.Router()
488        self.factory = component.XMPPComponentServerFactory(self.router,
489                                                            'secret')
490        self.xmlstream = self.factory.buildProtocol(None)
491        self.xmlstream.thisEntity = JID('component.example.org')
492
493
494    def test_makeConnection(self):
495        """
496        A new connection increases the stream serial count. No logs by default.
497        """
498        self.xmlstream.dispatch(self.xmlstream,
499                                xmlstream.STREAM_CONNECTED_EVENT)
500        self.assertEqual(0, self.xmlstream.serial)
501        self.assertEqual(1, self.factory.serial)
502        self.assertIdentical(None, self.xmlstream.rawDataInFn)
503        self.assertIdentical(None, self.xmlstream.rawDataOutFn)
504
505
506    def test_makeConnectionLogTraffic(self):
507        """
508        Setting logTraffic should set up raw data loggers.
509        """
510        self.factory.logTraffic = True
511        self.xmlstream.dispatch(self.xmlstream,
512                                xmlstream.STREAM_CONNECTED_EVENT)
513        self.assertNotIdentical(None, self.xmlstream.rawDataInFn)
514        self.assertNotIdentical(None, self.xmlstream.rawDataOutFn)
515
516
517    def test_onError(self):
518        """
519        An observer for stream errors should trigger onError to log it.
520        """
521        self.xmlstream.dispatch(self.xmlstream,
522                                xmlstream.STREAM_CONNECTED_EVENT)
523
524        class TestError(Exception):
525            pass
526
527        reason = failure.Failure(TestError())
528        self.xmlstream.dispatch(reason, xmlstream.STREAM_ERROR_EVENT)
529        self.assertEqual(1, len(self.flushLoggedErrors(TestError)))
530
531
532    def test_connectionInitialized(self):
533        """
534        Make sure a new stream is added to the routing table.
535        """
536        self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
537        self.assertIn('component.example.org', self.router.routes)
538        self.assertIdentical(self.xmlstream,
539                             self.router.routes['component.example.org'])
540
541
542    def test_connectionLost(self):
543        """
544        Make sure a stream is removed from the routing table on disconnect.
545        """
546        self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
547        self.xmlstream.dispatch(None, xmlstream.STREAM_END_EVENT)
548        self.assertNotIn('component.example.org', self.router.routes)
Note: See TracBrowser for help on using the repository browser.