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