source: ralphm-patches/listening-authenticator-stream-features.patch @ 79:0752a1cca356

Last change on this file since 79:0752a1cca356 was 79:0752a1cca356, checked in by Ralph Meijer <ralphm@…>, 4 years ago

Split out stanza module and response tracking patches, fix other patches.

File size: 24.7 KB
RevLine 
[64]1# HG changeset patch
[72]2# Parent 840b96390047670c5209195300f902689c18b12f
[79]3# Parent  146ff13d66e54c535445adf4a19a3ef07a0a6f57
[65]4Add FeatureListeningAuthenticator.
5
6This new authenticator is for incoming streams and uses initializers
7for stream negotiation, similar to inializers for clients.
8
[64]9diff --git a/wokkel/generic.py b/wokkel/generic.py
10--- a/wokkel/generic.py
11+++ b/wokkel/generic.py
[79]12@@ -14,6 +14,7 @@
[66]13 from zope.interface import implements
14 
15 from twisted.internet import defer, protocol
[79]16+from twisted.python import log
[72]17 from twisted.python.deprecate import deprecated
18 from twisted.python.versions import Version
[79]19 from twisted.words.protocols.jabber import error, xmlstream
20@@ -21,7 +22,7 @@
[66]21 from twisted.words.xish import domish, utility
22 from twisted.words.xish.xmlstream import BootstrapMixin
23 
24-from wokkel.iwokkel import IDisco
25+from wokkel.iwokkel import IDisco, IReceivingInitializer
[79]26 from wokkel.stanza import Stanza, ErrorStanza, Request
[66]27 from wokkel.subprotocols import XMPPHandler
28 
[79]29@@ -31,6 +32,8 @@
[65]30 NS_VERSION = 'jabber:iq:version'
31 VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]'
32 
33+XPATH_ALL = "/*"
34+
35 def parseXml(string):
36     """
37     Parse serialized XML into a DOM structure.
[79]38@@ -180,6 +183,290 @@
[64]39 
[72]40 
41 
[64]42+class TestableXmlStream(xmlstream.XmlStream):
[66]43+    """
44+    XML Stream that buffers outgoing data and catches special events.
45+
46+    This implementation overrides relevant methods to prevent any data
47+    to be sent out on a transport. Instead it buffers all outgoing stanzas,
48+    sets flags instead of sending the stream header and footer and logs stream
49+    errors so it can be caught by a logging observer.
50+
51+    @ivar output: Sequence of objects sent out using L{send}. Usually these are
52+        L{domish.Element} instances.
53+    @type output: C{list}
54+
55+    @ivar headerSent: Flag set when a stream header would have been sent. When
56+        a stream restart occurs through L{reset}, this flag is reset as well.
57+    @type headerSent: C{bool}
58+
59+    @ivar footerSent: Flag set when a stream footer would have been sent
60+        explicitly. Note that it is not set when a stream error is sent
61+        using L{sendStreamError}.
62+    @type footerSent: C{bool}
63+    """
64+
[64]65+
66+    def __init__(self, authenticator):
67+        xmlstream.XmlStream.__init__(self, authenticator)
68+        self.headerSent = False
69+        self.footerSent = False
70+        self.output = []
71+
72+
73+    def reset(self):
74+        xmlstream.XmlStream.reset(self)
75+        self.headerSent = False
76+
77+
78+    def sendHeader(self):
79+        self.headerSent = True
80+
81+
82+    def sendFooter(self):
83+        self.footerSent = True
84+
85+
86+    def sendStreamError(self, streamError):
[66]87+        """
88+        Log a stream error.
89+
90+        If this is called from a Twisted Trial test case, the stream error
91+        will be observed by the Trial logging observer. If it is not explicitly
92+        tested for (i.e. flushed), this will cause the test case to
93+        automatically fail. See L{assertStreamError} for a convenience method
94+        to test for stream errors.
95+
96+        @type streamError: L{error.StreamError}
97+        """
98+        log.err(streamError)
99+
100+
101+    @staticmethod
102+    def assertStreamError(testcase, condition=None, exc=None):
103+        """
104+        Check if a stream error was sent out.
105+
106+        To check for stream errors sent out by L{sendStreamError}, this method
107+        will flush logged stream errors and inspect the last one. If
108+        C{condition} was passed, the logged error is asserted to match that
109+        condition. If C{exc} was passed, the logged error is asserted to be
110+        identical to it.
111+
112+        Note that this is takes the calling test case as the first argument, to
113+        be able to hook into its methods for flushing errors and making
114+        assertions.
115+
116+        @param testcase: The test case instance that is calling this method.
117+        @type testcase: {twisted.trial.unittest.TestCase}
118+
119+        @param condition: The optional stream error condition to match against.
120+        @type condition: C{unicode}.
121+
122+        @param exc: The optional stream error to check identity against.
123+        @type exc: L{error.StreamError}
124+        """
125+
126+        loggedErrors = testcase.flushLoggedErrors(error.StreamError)
127+        testcase.assertTrue(loggedErrors, "No stream error was sent")
128+        streamError = loggedErrors[-1].value
129+        if condition:
130+            testcase.assertEqual(condition, streamError.condition)
131+        elif exc:
132+            testcase.assertIdentical(exc, streamError)
[64]133+
134+
135+    def send(self, obj):
[66]136+        """
137+        Buffer all outgoing stanzas.
138+
139+        @type obj: L{domish.Element}
140+        """
[64]141+        self.output.append(obj)
142+
143+
144+
[66]145+class BaseReceivingInitializer(object):
146+    """
147+    Base stream initializer for receiving entities.
148+    """
149+    implements(IReceivingInitializer)
150+
151+    required = False
152+
153+    def __init__(self, name, xs):
154+        self.name = name
155+        self.xmlstream = xs
156+        self.deferred = defer.Deferred()
157+
158+
159+    def getFeatures(self):
160+        raise NotImplementedError()
161+
162+
163+    def initialize(self):
164+        return self.deferred
165+
166+
167+
[64]168+class FeatureListenAuthenticator(xmlstream.ListenAuthenticator):
169+    """
170+    Authenticator for receiving entities with support for initializers.
171+    """
172+
[65]173+    def __init__(self):
174+        self.completedInitializers = []
175+
176+
177+    def _onElementFallback(self, element):
178+        """
179+        Fallback observer that rejects XML Stanzas.
180+
181+        This observer is active while stream feature negotiation has not yet
182+        completed.
183+        """
184+        # ignore elements that are not XML Stanzas
185+        if (element.uri not in (self.namespace) or
186+            element.name not in ('iq', 'message', 'presence')):
187+            return
188+
189+        # ignore elements that have already been handled
190+        if element.handled:
191+            return
192+
193+        exc = error.StreamError('not-authorized')
194+        self.xmlstream.sendStreamError(exc)
195+
196+
[66]197+    def connectionMade(self):
198+        """
199+        Called when the connection has been made.
200+
201+        Adds an observer to reject XML Stanzas until stream feature negotiation
202+        has completed.
203+        """
204+        xmlstream.ListenAuthenticator.connectionMade(self)
205+        self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1)
206+
207+
208+    def _cbInit(self, result):
209+        """
210+        Mark the initializer as completed and continue to the next.
211+        """
212+        result, index = result
213+        self.completedInitializers.append(self._initializers[index].name)
214+        del self._initializers[index]
215+
216+        if result is xmlstream.Reset:
217+            # The initializer initiated a stream restart, bail.
218+            return
219+        else:
[64]220+            self._initializeStream()
221+
[66]222+
223+    def _ebInit(self, failure):
224+        """
225+        Called when an initializer raises an exception.
226+
227+        If the exception is a L{error.StreamError} it is sent out, otherwise
228+        the error is logged and a stream error with condition
229+        C{'internal-server-error'} is sent out instead.
230+        """
231+        firstError = failure.value
232+        subFailure = firstError.subFailure
233+        if subFailure.check(error.StreamError):
234+            exc = subFailure.value
235+        else:
236+            log.err(subFailure, index=firstError.index)
237+            exc = error.StreamError('internal-server-error')
238+
239+        self.xmlstream.sendStreamError(exc)
240+
241+
242+    def _initializeStream(self):
243+        """
244+        Initialize the stream.
245+
246+        This walks all initializers to retrieve their features and determine
247+        if there is at least one required initializer. If not, the stream is
248+        ready for the exchange of stanzas. The features are sent out and each
249+        initializer will have its C{initialize} method called.
250+
251+        If negation has completed, L{xmlstream.STREAM_AUTHD_EVENT} is
252+        dispatched and the observer rejecting incoming stanzas is removed.
253+        """
[64]254+        features = domish.Element((xmlstream.NS_STREAMS, 'features'))
255+        ds = []
256+        required = False
[65]257+        for initializer in self._initializers:
[64]258+            required = required or initializer.required
259+            for feature in initializer.getFeatures():
260+                features.addChild(feature)
261+            d = initializer.initialize()
262+            ds.append(d)
[66]263+
[64]264+        self.xmlstream.send(features)
[66]265+
[64]266+        if not required:
[66]267+            # There are no required initializers anymore. This stream is
268+            # now ready for the exchange of stanzas.
[65]269+            self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback)
[64]270+            self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
[66]271+
[64]272+        if ds:
[66]273+            d = defer.DeferredList(ds, fireOnOneCallback=True,
274+                                       fireOnOneErrback=True,
275+                                       consumeErrors=True)
276+            d.addCallbacks(self._cbInit, self._ebInit)
277+
[64]278+
[65]279+    def getInitializers(self):
[66]280+        """
281+        Get the initializers for the current stage of stream negotiation.
282+
283+        This will be called at the start of each stream start to retrieve
284+        the initializers for which the features are advertised and initiated.
285+
286+        @rtype: C{list} of C{IReceivingInitializer} instances.
287+        """
288+        raise NotImplementedError()
[65]289+
290+
291+    def checkStream(self):
[66]292+        """
293+        Check the stream before sending out a stream header and initialization.
294+
295+        This is the place to inspect the stream properties and raise a relevant
296+        L{error.StreamError} if needed.
297+        """
298+        # Check stream namespace
[65]299+        if self.xmlstream.namespace != self.namespace:
300+            self.xmlstream.namespace = self.namespace
301+            raise error.StreamError('invalid-namespace')
[64]302+
303+
304+    def streamStarted(self, rootElement):
[66]305+        """
306+        Called when the stream header has been received.
307+
308+        Check the stream properties through L{checkStream}, send out
309+        a stream header, and retrieve and initialize the stream initializers.
310+        """
[64]311+        xmlstream.ListenAuthenticator.streamStarted(self, rootElement)
[65]312+
313+        try:
314+            self.checkStream()
315+        except error.StreamError, exc:
316+            self.xmlstream.sendStreamError(exc)
317+            return
318+
[64]319+        self.xmlstream.sendHeader()
[65]320+
321+        self._initializers = self.getInitializers()
[64]322+        self._initializeStream()
[72]323+
324+
325+
326 @deprecated(Version("Wokkel", 0, 8, 0), "unicode.encode('idna')")
327 def prepareIDNName(name):
328     """
[66]329diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py
330--- a/wokkel/iwokkel.py
331+++ b/wokkel/iwokkel.py
332@@ -11,7 +11,7 @@
333            'IPubSubClient', 'IPubSubService', 'IPubSubResource',
334            'IMUCClient', 'IMUCStatuses']
335 
336-from zope.interface import Interface
337+from zope.interface import Attribute, Interface
338 from twisted.python.deprecate import deprecatedModuleAttribute
339 from twisted.python.versions import Version
340 from twisted.words.protocols.jabber.ijabber import IXMPPHandler
341@@ -982,3 +982,50 @@
342         """
343         Return the number of status conditions.
344         """
345+
346+
347+
348+class IReceivingInitializer(Interface):
349+    """
350+    Interface for XMPP stream initializers for receiving entities.
351+    """
352+
353+    required = Attribute(
354+        """
355+        This initializer is required to complete feature negotiation.
356+        """)
357+    name = Attribute(
358+        """
359+        Identifier for this initializer.
360+
361+        This identifier is included in
362+        L{wokkel.generic.FeatureListenAuthenticator} when an initializer has
363+        completed.
364+        """)
365+    xmlstream = Attribute(
366+        """
367+        The XML Stream.
368+        """)
369+    deferred = Attribute(
370+        """
371+        The deferred returned from initialize.
372+        """)
373+
374+
375+    def getFeatures():
376+        """
377+        Get stream features for this initializer.
378+
379+        @rtype: C{list} of L{twisted.words.xish.domish.Element}
380+        """
381+
382+
383+    def initialize():
384+        """
385+        Initialize the initializer.
386+
387+        This is where observers for feature negotiation are set up. When
388+        the returned deferred fires, it is assumed to have completed.
389+
390+        @rtype: L{twisted.internet.defer.Deferred}
391+        """
[64]392diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
393--- a/wokkel/test/test_generic.py
394+++ b/wokkel/test/test_generic.py
[72]395@@ -7,14 +7,19 @@
396 
397 import re
[64]398 
[66]399+from zope.interface import verify
400+
[64]401+from twisted.internet import defer
[72]402 from twisted.python import deprecate
403 from twisted.python.versions import Version
[64]404+from twisted.test import proto_helpers
405 from twisted.trial import unittest
[72]406 from twisted.trial.util import suppress as SUPPRESS
[64]407 from twisted.words.xish import domish
408 from twisted.words.protocols.jabber.jid import JID
[65]409+from twisted.words.protocols.jabber import error, xmlstream
[64]410 
[66]411-from wokkel import generic
412+from wokkel import generic, iwokkel
[64]413 from wokkel.test.helpers import XmlStreamStub
[66]414 
415 NS_VERSION = 'jabber:iq:version'
[79]416@@ -300,6 +305,334 @@
[72]417 
418 
419 
[66]420+class BaseReceivingInitializerTest(unittest.TestCase):
421+    """
422+    Tests for L{generic.BaseReceivingInitializer}.
423+    """
424+
425+    def setUp(self):
426+        self.init = generic.BaseReceivingInitializer('init', None)
427+
428+
429+    def test_interface(self):
430+        verify.verifyObject(iwokkel.IReceivingInitializer, self.init)
431+
432+
433+    def test_getFeatures(self):
434+        self.assertRaises(NotImplementedError, self.init.getFeatures)
435+
436+
437+    def test_initialize(self):
438+        d = self.init.initialize()
439+        self.init.deferred.callback(None)
440+        return d
441+
442+
443+
444+class TestableReceivingInitializer(generic.BaseReceivingInitializer):
[65]445+    """
446+    Testable initializer for receiving entities.
447+
448+    This initializer advertises support for a stream feature denoted by
449+    C{uri} and C{name}. Its C{deferred} should be fired to complete
450+    initialization for this initializer.
451+
452+    @ivar uri: Namespace of the stream feature.
[66]453+    @ivar localname: Element localname for the stream feature.
[65]454+    """
455+    required = True
456+
[66]457+    def __init__(self, name, xs, uri, localname):
458+        generic.BaseReceivingInitializer.__init__(self, name, xs)
[65]459+        self.uri = uri
[66]460+        self.localname = localname
[65]461+        self.deferred = defer.Deferred()
462+
463+
464+    def getFeatures(self):
[66]465+        return [domish.Element((self.uri, self.localname))]
[65]466+
467+
468+
[64]469+class FeatureListenAuthenticatorTest(unittest.TestCase):
470+    """
471+    Tests for L{generic.FeatureListenAuthenticator}.
472+    """
473+
474+    def setUp(self):
475+        self.gotAuthenticated = False
476+        self.initFailure = None
477+        self.authenticator = generic.FeatureListenAuthenticator()
[65]478+        self.authenticator.namespace = 'jabber:server'
[64]479+        self.xmlstream = generic.TestableXmlStream(self.authenticator)
480+        self.xmlstream.addObserver('//event/stream/authd',
481+                                   self.onAuthenticated)
482+        self.xmlstream.addObserver('//event/xmpp/initfailed',
483+                                   self.onInitFailed)
484+
[66]485+        self.init = TestableReceivingInitializer('init', self.xmlstream,
486+                                                 'testns', 'test')
[65]487+
488+        def getInitializers():
489+            return [self.init]
490+
491+        self.authenticator.getInitializers = getInitializers
492+
[64]493+
494+    def onAuthenticated(self, obj):
495+        self.gotAuthenticated = True
496+
497+
498+    def onInitFailed(self, failure):
499+        self.initFailure = failure
500+
501+
[66]502+    def test_getInitializers(self):
503+        """
504+        Unoverridden getInitializers raises NotImplementedError.
505+        """
506+        authenticator = generic.FeatureListenAuthenticator()
507+        self.assertRaises(
508+            NotImplementedError,
509+            authenticator.getInitializers)
510+
511+
[64]512+    def test_streamStarted(self):
513+        """
514+        Upon stream start, stream initializers are set up.
515+
516+        The method getInitializers will determine the available stream
517+        initializers given the current state of stream initialization.
[65]518+        hen, each of the returned initializers will be called to set
[64]519+        themselves up.
520+        """
521+        xs = self.xmlstream
522+        xs.makeConnection(proto_helpers.StringTransport())
523+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
524+                         "xmlns:stream='http://etherx.jabber.org/streams' "
525+                         "from='example.com' to='example.org' id='12345' "
526+                         "version='1.0'>")
527+
528+        self.assertTrue(xs.headerSent)
529+
530+        # Check if features were sent
531+        features = xs.output[-1]
532+        self.assertEquals(xmlstream.NS_STREAMS, features.uri)
533+        self.assertEquals('features', features.name)
534+        feature = features.elements().next()
535+        self.assertEqual('testns', feature.uri)
536+        self.assertEqual('test', feature.name)
537+
538+        self.assertFalse(self.gotAuthenticated)
539+
[65]540+        self.init.deferred.callback(None)
[64]541+        self.assertTrue(self.gotAuthenticated)
[65]542+
543+
[66]544+    def test_streamStartedStreamError(self):
545+        """
546+        A stream error raised by the initializer is sent out.
547+        """
548+        xs = self.xmlstream
549+        xs.makeConnection(proto_helpers.StringTransport())
550+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
551+                         "xmlns:stream='http://etherx.jabber.org/streams' "
552+                         "from='example.com' to='example.org' id='12345' "
553+                         "version='1.0'>")
554+
555+        self.assertTrue(xs.headerSent)
556+
557+        xs.output = []
558+        exc = error.StreamError('policy-violation')
559+        self.init.deferred.errback(exc)
560+
561+        self.xmlstream.assertStreamError(self, exc=exc)
562+        self.assertFalse(xs.output)
563+        self.assertFalse(self.gotAuthenticated)
564+
565+
566+    def test_streamStartedOtherError(self):
567+        """
568+        Initializer exceptions are logged and yield a internal-server-error.
569+        """
570+        xs = self.xmlstream
571+        xs.makeConnection(proto_helpers.StringTransport())
572+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
573+                         "xmlns:stream='http://etherx.jabber.org/streams' "
574+                         "from='example.com' to='example.org' id='12345' "
575+                         "version='1.0'>")
576+
577+        self.assertTrue(xs.headerSent)
578+
579+        xs.output = []
580+        class Error(Exception):
581+            pass
582+        self.init.deferred.errback(Error())
583+
584+        self.xmlstream.assertStreamError(self, condition='internal-server-error')
585+        self.assertFalse(xs.output)
586+        self.assertFalse(self.gotAuthenticated)
587+        self.assertEqual(1, len(self.flushLoggedErrors(Error)))
588+
589+
[65]590+    def test_streamStartedInitializerCompleted(self):
591+        """
592+        Succesfully finished initializers are recorded.
593+        """
594+        xs = self.xmlstream
595+        xs.makeConnection(proto_helpers.StringTransport())
596+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
597+                         "xmlns:stream='http://etherx.jabber.org/streams' "
598+                         "from='example.com' to='example.org' id='12345' "
599+                         "version='1.0'>")
600+
[66]601+        xs.output = []
[65]602+        self.init.deferred.callback(None)
[66]603+        self.assertEqual(['init'], self.authenticator.completedInitializers)
604+
605+
606+    def test_streamStartedInitializerCompletedFeatures(self):
607+        """
608+        After completing an initializer, stream features are sent again.
609+
610+        In this case, with only one initializer, there are no more features.
611+        """
612+        xs = self.xmlstream
613+        xs.makeConnection(proto_helpers.StringTransport())
614+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
615+                         "xmlns:stream='http://etherx.jabber.org/streams' "
616+                         "from='example.com' to='example.org' id='12345' "
617+                         "version='1.0'>")
618+
619+        xs.output = []
620+        self.init.deferred.callback(None)
621+
622+        self.assertEqual(1, len(xs.output))
623+        features = xs.output[-1]
624+        self.assertEqual('features', features.name)
625+        self.assertEqual(xmlstream.NS_STREAMS, features.uri)
626+        self.assertFalse(features.children)
627+
628+
629+    def test_streamStartedInitializerCompletedReset(self):
630+        """
631+        If an initializer completes with Reset, no features are sent.
632+        """
633+        xs = self.xmlstream
634+        xs.makeConnection(proto_helpers.StringTransport())
635+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
636+                         "xmlns:stream='http://etherx.jabber.org/streams' "
637+                         "from='example.com' to='example.org' id='12345' "
638+                         "version='1.0'>")
639+
640+        xs.output = []
641+        self.init.deferred.callback(xmlstream.Reset)
642+
643+        self.assertEqual(0, len(xs.output))
[65]644+
645+
646+    def test_streamStartedXmlStanzasRejected(self):
647+        """
648+        XML Stanzas may not be sent before feature negotiation has completed.
649+        """
650+        xs = self.xmlstream
651+        xs.makeConnection(proto_helpers.StringTransport())
652+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
653+                         "xmlns:stream='http://etherx.jabber.org/streams' "
654+                         "from='example.com' to='example.org' id='12345' "
655+                         "version='1.0'>")
656+
657+        xs.dataReceived("<iq to='example.org' from='example.com' type='set'>"
658+                        "  <query xmlns='jabber:iq:version'/>"
659+                        "</iq>")
660+
[66]661+        self.xmlstream.assertStreamError(self, condition='not-authorized')
[65]662+
663+
664+    def test_streamStartedCompleteXmlStanzasAllowed(self):
665+        """
666+        XML Stanzas may sent after feature negotiation has completed.
667+        """
668+        xs = self.xmlstream
669+        xs.makeConnection(proto_helpers.StringTransport())
670+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
671+                         "xmlns:stream='http://etherx.jabber.org/streams' "
672+                         "from='example.com' to='example.org' id='12345' "
673+                         "version='1.0'>")
674+
675+        self.init.deferred.callback(None)
676+
677+        xs.dataReceived("<iq to='example.org' from='example.com' type='set'>"
678+                        "  <query xmlns='jabber:iq:version'/>"
679+                        "</iq>")
680+
681+
682+    def test_streamStartedXmlStanzasHandledIgnored(self):
683+        """
684+        XML Stanzas that have already been handled are ignored.
685+        """
686+        xs = self.xmlstream
687+        xs.makeConnection(proto_helpers.StringTransport())
688+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
689+                         "xmlns:stream='http://etherx.jabber.org/streams' "
690+                         "from='example.com' to='example.org' id='12345' "
691+                         "version='1.0'>")
692+
693+        iq = generic.parseXml("<iq to='example.org' from='example.com' type='set'>"
694+                              "  <query xmlns='jabber:iq:version'/>"
695+                              "</iq>")
696+        iq.handled = True
697+        xs.dispatch(iq)
698+
699+
700+    def test_streamStartedNonXmlStanzasIgnored(self):
701+        """
702+        Elements that are not XML Stranzas are not rejected.
703+        """
704+        xs = self.xmlstream
705+        xs.makeConnection(proto_helpers.StringTransport())
706+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
707+                         "xmlns:stream='http://etherx.jabber.org/streams' "
708+                         "from='example.com' to='example.org' id='12345' "
709+                         "version='1.0'>")
710+
711+        xs.dataReceived("<test xmlns='myns'/>")
712+
713+
714+    def test_streamStartedCheckStream(self):
715+        """
716+        Stream errors raised by checkStream are sent out.
717+        """
718+        def checkStream():
719+            raise error.StreamError('undefined-condition')
720+
721+        self.authenticator.checkStream = checkStream
722+        xs = self.xmlstream
723+        xs.makeConnection(proto_helpers.StringTransport())
724+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
725+                         "xmlns:stream='http://etherx.jabber.org/streams' "
726+                         "from='example.com' to='example.org' id='12345' "
727+                         "version='1.0'>")
728+
[66]729+        self.xmlstream.assertStreamError(self, condition='undefined-condition')
[65]730+        self.assertFalse(xs.headerSent)
731+
732+
733+    def test_checkStreamNamespace(self):
734+        """
735+        The stream namespace must match the pre-defined stream namespace.
736+        """
737+        xs = self.xmlstream
738+        xs.makeConnection(proto_helpers.StringTransport())
739+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
740+                         "xmlns:stream='http://etherx.jabber.org/streams' "
741+                         "from='example.com' to='example.org' id='12345' "
742+                         "version='1.0'>")
743+
[66]744+        self.xmlstream.assertStreamError(self, condition='invalid-namespace')
[72]745+
746+
747+
748 class PrepareIDNNameTests(unittest.TestCase):
749     """
750     Tests for L{wokkel.generic.prepareIDNName}.
Note: See TracBrowser for help on using the repository browser.