source: wokkel/test/test_component.py @ 247:290573dcb97a

18.0.0
Last change on this file since 247:290573dcb97a was 247:290573dcb97a, checked in by Ralph Meijer <ralphm@…>, 16 months ago

Add missing method from interface to fake

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