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
  • wokkel/generic.py

    # HG changeset patch
    # Parent 840b96390047670c5209195300f902689c18b12f
    # Parent  146ff13d66e54c535445adf4a19a3ef07a0a6f57
    Add FeatureListeningAuthenticator.
    
    This new authenticator is for incoming streams and uses initializers
    for stream negotiation, similar to inializers for clients.
    
    diff --git a/wokkel/generic.py b/wokkel/generic.py
    a b  
    1414from zope.interface import implements
    1515
    1616from twisted.internet import defer, protocol
     17from twisted.python import log
    1718from twisted.python.deprecate import deprecated
    1819from twisted.python.versions import Version
    1920from twisted.words.protocols.jabber import error, xmlstream
     
    2122from twisted.words.xish import domish, utility
    2223from twisted.words.xish.xmlstream import BootstrapMixin
    2324
    24 from wokkel.iwokkel import IDisco
     25from wokkel.iwokkel import IDisco, IReceivingInitializer
    2526from wokkel.stanza import Stanza, ErrorStanza, Request
    2627from wokkel.subprotocols import XMPPHandler
    2728
     
    3132NS_VERSION = 'jabber:iq:version'
    3233VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]'
    3334
     35XPATH_ALL = "/*"
     36
    3437def parseXml(string):
    3538    """
    3639    Parse serialized XML into a DOM structure.
     
    180183
    181184
    182185
     186class TestableXmlStream(xmlstream.XmlStream):
     187    """
     188    XML Stream that buffers outgoing data and catches special events.
     189
     190    This implementation overrides relevant methods to prevent any data
     191    to be sent out on a transport. Instead it buffers all outgoing stanzas,
     192    sets flags instead of sending the stream header and footer and logs stream
     193    errors so it can be caught by a logging observer.
     194
     195    @ivar output: Sequence of objects sent out using L{send}. Usually these are
     196        L{domish.Element} instances.
     197    @type output: C{list}
     198
     199    @ivar headerSent: Flag set when a stream header would have been sent. When
     200        a stream restart occurs through L{reset}, this flag is reset as well.
     201    @type headerSent: C{bool}
     202
     203    @ivar footerSent: Flag set when a stream footer would have been sent
     204        explicitly. Note that it is not set when a stream error is sent
     205        using L{sendStreamError}.
     206    @type footerSent: C{bool}
     207    """
     208
     209
     210    def __init__(self, authenticator):
     211        xmlstream.XmlStream.__init__(self, authenticator)
     212        self.headerSent = False
     213        self.footerSent = False
     214        self.output = []
     215
     216
     217    def reset(self):
     218        xmlstream.XmlStream.reset(self)
     219        self.headerSent = False
     220
     221
     222    def sendHeader(self):
     223        self.headerSent = True
     224
     225
     226    def sendFooter(self):
     227        self.footerSent = True
     228
     229
     230    def sendStreamError(self, streamError):
     231        """
     232        Log a stream error.
     233
     234        If this is called from a Twisted Trial test case, the stream error
     235        will be observed by the Trial logging observer. If it is not explicitly
     236        tested for (i.e. flushed), this will cause the test case to
     237        automatically fail. See L{assertStreamError} for a convenience method
     238        to test for stream errors.
     239
     240        @type streamError: L{error.StreamError}
     241        """
     242        log.err(streamError)
     243
     244
     245    @staticmethod
     246    def assertStreamError(testcase, condition=None, exc=None):
     247        """
     248        Check if a stream error was sent out.
     249
     250        To check for stream errors sent out by L{sendStreamError}, this method
     251        will flush logged stream errors and inspect the last one. If
     252        C{condition} was passed, the logged error is asserted to match that
     253        condition. If C{exc} was passed, the logged error is asserted to be
     254        identical to it.
     255
     256        Note that this is takes the calling test case as the first argument, to
     257        be able to hook into its methods for flushing errors and making
     258        assertions.
     259
     260        @param testcase: The test case instance that is calling this method.
     261        @type testcase: {twisted.trial.unittest.TestCase}
     262
     263        @param condition: The optional stream error condition to match against.
     264        @type condition: C{unicode}.
     265
     266        @param exc: The optional stream error to check identity against.
     267        @type exc: L{error.StreamError}
     268        """
     269
     270        loggedErrors = testcase.flushLoggedErrors(error.StreamError)
     271        testcase.assertTrue(loggedErrors, "No stream error was sent")
     272        streamError = loggedErrors[-1].value
     273        if condition:
     274            testcase.assertEqual(condition, streamError.condition)
     275        elif exc:
     276            testcase.assertIdentical(exc, streamError)
     277
     278
     279    def send(self, obj):
     280        """
     281        Buffer all outgoing stanzas.
     282
     283        @type obj: L{domish.Element}
     284        """
     285        self.output.append(obj)
     286
     287
     288
     289class BaseReceivingInitializer(object):
     290    """
     291    Base stream initializer for receiving entities.
     292    """
     293    implements(IReceivingInitializer)
     294
     295    required = False
     296
     297    def __init__(self, name, xs):
     298        self.name = name
     299        self.xmlstream = xs
     300        self.deferred = defer.Deferred()
     301
     302
     303    def getFeatures(self):
     304        raise NotImplementedError()
     305
     306
     307    def initialize(self):
     308        return self.deferred
     309
     310
     311
     312class FeatureListenAuthenticator(xmlstream.ListenAuthenticator):
     313    """
     314    Authenticator for receiving entities with support for initializers.
     315    """
     316
     317    def __init__(self):
     318        self.completedInitializers = []
     319
     320
     321    def _onElementFallback(self, element):
     322        """
     323        Fallback observer that rejects XML Stanzas.
     324
     325        This observer is active while stream feature negotiation has not yet
     326        completed.
     327        """
     328        # ignore elements that are not XML Stanzas
     329        if (element.uri not in (self.namespace) or
     330            element.name not in ('iq', 'message', 'presence')):
     331            return
     332
     333        # ignore elements that have already been handled
     334        if element.handled:
     335            return
     336
     337        exc = error.StreamError('not-authorized')
     338        self.xmlstream.sendStreamError(exc)
     339
     340
     341    def connectionMade(self):
     342        """
     343        Called when the connection has been made.
     344
     345        Adds an observer to reject XML Stanzas until stream feature negotiation
     346        has completed.
     347        """
     348        xmlstream.ListenAuthenticator.connectionMade(self)
     349        self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1)
     350
     351
     352    def _cbInit(self, result):
     353        """
     354        Mark the initializer as completed and continue to the next.
     355        """
     356        result, index = result
     357        self.completedInitializers.append(self._initializers[index].name)
     358        del self._initializers[index]
     359
     360        if result is xmlstream.Reset:
     361            # The initializer initiated a stream restart, bail.
     362            return
     363        else:
     364            self._initializeStream()
     365
     366
     367    def _ebInit(self, failure):
     368        """
     369        Called when an initializer raises an exception.
     370
     371        If the exception is a L{error.StreamError} it is sent out, otherwise
     372        the error is logged and a stream error with condition
     373        C{'internal-server-error'} is sent out instead.
     374        """
     375        firstError = failure.value
     376        subFailure = firstError.subFailure
     377        if subFailure.check(error.StreamError):
     378            exc = subFailure.value
     379        else:
     380            log.err(subFailure, index=firstError.index)
     381            exc = error.StreamError('internal-server-error')
     382
     383        self.xmlstream.sendStreamError(exc)
     384
     385
     386    def _initializeStream(self):
     387        """
     388        Initialize the stream.
     389
     390        This walks all initializers to retrieve their features and determine
     391        if there is at least one required initializer. If not, the stream is
     392        ready for the exchange of stanzas. The features are sent out and each
     393        initializer will have its C{initialize} method called.
     394
     395        If negation has completed, L{xmlstream.STREAM_AUTHD_EVENT} is
     396        dispatched and the observer rejecting incoming stanzas is removed.
     397        """
     398        features = domish.Element((xmlstream.NS_STREAMS, 'features'))
     399        ds = []
     400        required = False
     401        for initializer in self._initializers:
     402            required = required or initializer.required
     403            for feature in initializer.getFeatures():
     404                features.addChild(feature)
     405            d = initializer.initialize()
     406            ds.append(d)
     407
     408        self.xmlstream.send(features)
     409
     410        if not required:
     411            # There are no required initializers anymore. This stream is
     412            # now ready for the exchange of stanzas.
     413            self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback)
     414            self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
     415
     416        if ds:
     417            d = defer.DeferredList(ds, fireOnOneCallback=True,
     418                                       fireOnOneErrback=True,
     419                                       consumeErrors=True)
     420            d.addCallbacks(self._cbInit, self._ebInit)
     421
     422
     423    def getInitializers(self):
     424        """
     425        Get the initializers for the current stage of stream negotiation.
     426
     427        This will be called at the start of each stream start to retrieve
     428        the initializers for which the features are advertised and initiated.
     429
     430        @rtype: C{list} of C{IReceivingInitializer} instances.
     431        """
     432        raise NotImplementedError()
     433
     434
     435    def checkStream(self):
     436        """
     437        Check the stream before sending out a stream header and initialization.
     438
     439        This is the place to inspect the stream properties and raise a relevant
     440        L{error.StreamError} if needed.
     441        """
     442        # Check stream namespace
     443        if self.xmlstream.namespace != self.namespace:
     444            self.xmlstream.namespace = self.namespace
     445            raise error.StreamError('invalid-namespace')
     446
     447
     448    def streamStarted(self, rootElement):
     449        """
     450        Called when the stream header has been received.
     451
     452        Check the stream properties through L{checkStream}, send out
     453        a stream header, and retrieve and initialize the stream initializers.
     454        """
     455        xmlstream.ListenAuthenticator.streamStarted(self, rootElement)
     456
     457        try:
     458            self.checkStream()
     459        except error.StreamError, exc:
     460            self.xmlstream.sendStreamError(exc)
     461            return
     462
     463        self.xmlstream.sendHeader()
     464
     465        self._initializers = self.getInitializers()
     466        self._initializeStream()
     467
     468
     469
    183470@deprecated(Version("Wokkel", 0, 8, 0), "unicode.encode('idna')")
    184471def prepareIDNName(name):
    185472    """
  • wokkel/iwokkel.py

    diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py
    a b  
    1111           'IPubSubClient', 'IPubSubService', 'IPubSubResource',
    1212           'IMUCClient', 'IMUCStatuses']
    1313
    14 from zope.interface import Interface
     14from zope.interface import Attribute, Interface
    1515from twisted.python.deprecate import deprecatedModuleAttribute
    1616from twisted.python.versions import Version
    1717from twisted.words.protocols.jabber.ijabber import IXMPPHandler
     
    982982        """
    983983        Return the number of status conditions.
    984984        """
     985
     986
     987
     988class IReceivingInitializer(Interface):
     989    """
     990    Interface for XMPP stream initializers for receiving entities.
     991    """
     992
     993    required = Attribute(
     994        """
     995        This initializer is required to complete feature negotiation.
     996        """)
     997    name = Attribute(
     998        """
     999        Identifier for this initializer.
     1000
     1001        This identifier is included in
     1002        L{wokkel.generic.FeatureListenAuthenticator} when an initializer has
     1003        completed.
     1004        """)
     1005    xmlstream = Attribute(
     1006        """
     1007        The XML Stream.
     1008        """)
     1009    deferred = Attribute(
     1010        """
     1011        The deferred returned from initialize.
     1012        """)
     1013
     1014
     1015    def getFeatures():
     1016        """
     1017        Get stream features for this initializer.
     1018
     1019        @rtype: C{list} of L{twisted.words.xish.domish.Element}
     1020        """
     1021
     1022
     1023    def initialize():
     1024        """
     1025        Initialize the initializer.
     1026
     1027        This is where observers for feature negotiation are set up. When
     1028        the returned deferred fires, it is assumed to have completed.
     1029
     1030        @rtype: L{twisted.internet.defer.Deferred}
     1031        """
  • wokkel/test/test_generic.py

    diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
    a b  
    77
    88import re
    99
     10from zope.interface import verify
     11
     12from twisted.internet import defer
    1013from twisted.python import deprecate
    1114from twisted.python.versions import Version
     15from twisted.test import proto_helpers
    1216from twisted.trial import unittest
    1317from twisted.trial.util import suppress as SUPPRESS
    1418from twisted.words.xish import domish
    1519from twisted.words.protocols.jabber.jid import JID
     20from twisted.words.protocols.jabber import error, xmlstream
    1621
    17 from wokkel import generic
     22from wokkel import generic, iwokkel
    1823from wokkel.test.helpers import XmlStreamStub
    1924
    2025NS_VERSION = 'jabber:iq:version'
     
    300305
    301306
    302307
     308class BaseReceivingInitializerTest(unittest.TestCase):
     309    """
     310    Tests for L{generic.BaseReceivingInitializer}.
     311    """
     312
     313    def setUp(self):
     314        self.init = generic.BaseReceivingInitializer('init', None)
     315
     316
     317    def test_interface(self):
     318        verify.verifyObject(iwokkel.IReceivingInitializer, self.init)
     319
     320
     321    def test_getFeatures(self):
     322        self.assertRaises(NotImplementedError, self.init.getFeatures)
     323
     324
     325    def test_initialize(self):
     326        d = self.init.initialize()
     327        self.init.deferred.callback(None)
     328        return d
     329
     330
     331
     332class TestableReceivingInitializer(generic.BaseReceivingInitializer):
     333    """
     334    Testable initializer for receiving entities.
     335
     336    This initializer advertises support for a stream feature denoted by
     337    C{uri} and C{name}. Its C{deferred} should be fired to complete
     338    initialization for this initializer.
     339
     340    @ivar uri: Namespace of the stream feature.
     341    @ivar localname: Element localname for the stream feature.
     342    """
     343    required = True
     344
     345    def __init__(self, name, xs, uri, localname):
     346        generic.BaseReceivingInitializer.__init__(self, name, xs)
     347        self.uri = uri
     348        self.localname = localname
     349        self.deferred = defer.Deferred()
     350
     351
     352    def getFeatures(self):
     353        return [domish.Element((self.uri, self.localname))]
     354
     355
     356
     357class FeatureListenAuthenticatorTest(unittest.TestCase):
     358    """
     359    Tests for L{generic.FeatureListenAuthenticator}.
     360    """
     361
     362    def setUp(self):
     363        self.gotAuthenticated = False
     364        self.initFailure = None
     365        self.authenticator = generic.FeatureListenAuthenticator()
     366        self.authenticator.namespace = 'jabber:server'
     367        self.xmlstream = generic.TestableXmlStream(self.authenticator)
     368        self.xmlstream.addObserver('//event/stream/authd',
     369                                   self.onAuthenticated)
     370        self.xmlstream.addObserver('//event/xmpp/initfailed',
     371                                   self.onInitFailed)
     372
     373        self.init = TestableReceivingInitializer('init', self.xmlstream,
     374                                                 'testns', 'test')
     375
     376        def getInitializers():
     377            return [self.init]
     378
     379        self.authenticator.getInitializers = getInitializers
     380
     381
     382    def onAuthenticated(self, obj):
     383        self.gotAuthenticated = True
     384
     385
     386    def onInitFailed(self, failure):
     387        self.initFailure = failure
     388
     389
     390    def test_getInitializers(self):
     391        """
     392        Unoverridden getInitializers raises NotImplementedError.
     393        """
     394        authenticator = generic.FeatureListenAuthenticator()
     395        self.assertRaises(
     396            NotImplementedError,
     397            authenticator.getInitializers)
     398
     399
     400    def test_streamStarted(self):
     401        """
     402        Upon stream start, stream initializers are set up.
     403
     404        The method getInitializers will determine the available stream
     405        initializers given the current state of stream initialization.
     406        hen, each of the returned initializers will be called to set
     407        themselves up.
     408        """
     409        xs = self.xmlstream
     410        xs.makeConnection(proto_helpers.StringTransport())
     411        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     412                         "xmlns:stream='http://etherx.jabber.org/streams' "
     413                         "from='example.com' to='example.org' id='12345' "
     414                         "version='1.0'>")
     415
     416        self.assertTrue(xs.headerSent)
     417
     418        # Check if features were sent
     419        features = xs.output[-1]
     420        self.assertEquals(xmlstream.NS_STREAMS, features.uri)
     421        self.assertEquals('features', features.name)
     422        feature = features.elements().next()
     423        self.assertEqual('testns', feature.uri)
     424        self.assertEqual('test', feature.name)
     425
     426        self.assertFalse(self.gotAuthenticated)
     427
     428        self.init.deferred.callback(None)
     429        self.assertTrue(self.gotAuthenticated)
     430
     431
     432    def test_streamStartedStreamError(self):
     433        """
     434        A stream error raised by the initializer is sent out.
     435        """
     436        xs = self.xmlstream
     437        xs.makeConnection(proto_helpers.StringTransport())
     438        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     439                         "xmlns:stream='http://etherx.jabber.org/streams' "
     440                         "from='example.com' to='example.org' id='12345' "
     441                         "version='1.0'>")
     442
     443        self.assertTrue(xs.headerSent)
     444
     445        xs.output = []
     446        exc = error.StreamError('policy-violation')
     447        self.init.deferred.errback(exc)
     448
     449        self.xmlstream.assertStreamError(self, exc=exc)
     450        self.assertFalse(xs.output)
     451        self.assertFalse(self.gotAuthenticated)
     452
     453
     454    def test_streamStartedOtherError(self):
     455        """
     456        Initializer exceptions are logged and yield a internal-server-error.
     457        """
     458        xs = self.xmlstream
     459        xs.makeConnection(proto_helpers.StringTransport())
     460        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     461                         "xmlns:stream='http://etherx.jabber.org/streams' "
     462                         "from='example.com' to='example.org' id='12345' "
     463                         "version='1.0'>")
     464
     465        self.assertTrue(xs.headerSent)
     466
     467        xs.output = []
     468        class Error(Exception):
     469            pass
     470        self.init.deferred.errback(Error())
     471
     472        self.xmlstream.assertStreamError(self, condition='internal-server-error')
     473        self.assertFalse(xs.output)
     474        self.assertFalse(self.gotAuthenticated)
     475        self.assertEqual(1, len(self.flushLoggedErrors(Error)))
     476
     477
     478    def test_streamStartedInitializerCompleted(self):
     479        """
     480        Succesfully finished initializers are recorded.
     481        """
     482        xs = self.xmlstream
     483        xs.makeConnection(proto_helpers.StringTransport())
     484        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     485                         "xmlns:stream='http://etherx.jabber.org/streams' "
     486                         "from='example.com' to='example.org' id='12345' "
     487                         "version='1.0'>")
     488
     489        xs.output = []
     490        self.init.deferred.callback(None)
     491        self.assertEqual(['init'], self.authenticator.completedInitializers)
     492
     493
     494    def test_streamStartedInitializerCompletedFeatures(self):
     495        """
     496        After completing an initializer, stream features are sent again.
     497
     498        In this case, with only one initializer, there are no more features.
     499        """
     500        xs = self.xmlstream
     501        xs.makeConnection(proto_helpers.StringTransport())
     502        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     503                         "xmlns:stream='http://etherx.jabber.org/streams' "
     504                         "from='example.com' to='example.org' id='12345' "
     505                         "version='1.0'>")
     506
     507        xs.output = []
     508        self.init.deferred.callback(None)
     509
     510        self.assertEqual(1, len(xs.output))
     511        features = xs.output[-1]
     512        self.assertEqual('features', features.name)
     513        self.assertEqual(xmlstream.NS_STREAMS, features.uri)
     514        self.assertFalse(features.children)
     515
     516
     517    def test_streamStartedInitializerCompletedReset(self):
     518        """
     519        If an initializer completes with Reset, no features are sent.
     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        xs.output = []
     529        self.init.deferred.callback(xmlstream.Reset)
     530
     531        self.assertEqual(0, len(xs.output))
     532
     533
     534    def test_streamStartedXmlStanzasRejected(self):
     535        """
     536        XML Stanzas may not be sent before feature negotiation has completed.
     537        """
     538        xs = self.xmlstream
     539        xs.makeConnection(proto_helpers.StringTransport())
     540        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     541                         "xmlns:stream='http://etherx.jabber.org/streams' "
     542                         "from='example.com' to='example.org' id='12345' "
     543                         "version='1.0'>")
     544
     545        xs.dataReceived("<iq to='example.org' from='example.com' type='set'>"
     546                        "  <query xmlns='jabber:iq:version'/>"
     547                        "</iq>")
     548
     549        self.xmlstream.assertStreamError(self, condition='not-authorized')
     550
     551
     552    def test_streamStartedCompleteXmlStanzasAllowed(self):
     553        """
     554        XML Stanzas may sent after feature negotiation has completed.
     555        """
     556        xs = self.xmlstream
     557        xs.makeConnection(proto_helpers.StringTransport())
     558        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     559                         "xmlns:stream='http://etherx.jabber.org/streams' "
     560                         "from='example.com' to='example.org' id='12345' "
     561                         "version='1.0'>")
     562
     563        self.init.deferred.callback(None)
     564
     565        xs.dataReceived("<iq to='example.org' from='example.com' type='set'>"
     566                        "  <query xmlns='jabber:iq:version'/>"
     567                        "</iq>")
     568
     569
     570    def test_streamStartedXmlStanzasHandledIgnored(self):
     571        """
     572        XML Stanzas that have already been handled are ignored.
     573        """
     574        xs = self.xmlstream
     575        xs.makeConnection(proto_helpers.StringTransport())
     576        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     577                         "xmlns:stream='http://etherx.jabber.org/streams' "
     578                         "from='example.com' to='example.org' id='12345' "
     579                         "version='1.0'>")
     580
     581        iq = generic.parseXml("<iq to='example.org' from='example.com' type='set'>"
     582                              "  <query xmlns='jabber:iq:version'/>"
     583                              "</iq>")
     584        iq.handled = True
     585        xs.dispatch(iq)
     586
     587
     588    def test_streamStartedNonXmlStanzasIgnored(self):
     589        """
     590        Elements that are not XML Stranzas are not rejected.
     591        """
     592        xs = self.xmlstream
     593        xs.makeConnection(proto_helpers.StringTransport())
     594        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     595                         "xmlns:stream='http://etherx.jabber.org/streams' "
     596                         "from='example.com' to='example.org' id='12345' "
     597                         "version='1.0'>")
     598
     599        xs.dataReceived("<test xmlns='myns'/>")
     600
     601
     602    def test_streamStartedCheckStream(self):
     603        """
     604        Stream errors raised by checkStream are sent out.
     605        """
     606        def checkStream():
     607            raise error.StreamError('undefined-condition')
     608
     609        self.authenticator.checkStream = checkStream
     610        xs = self.xmlstream
     611        xs.makeConnection(proto_helpers.StringTransport())
     612        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     613                         "xmlns:stream='http://etherx.jabber.org/streams' "
     614                         "from='example.com' to='example.org' id='12345' "
     615                         "version='1.0'>")
     616
     617        self.xmlstream.assertStreamError(self, condition='undefined-condition')
     618        self.assertFalse(xs.headerSent)
     619
     620
     621    def test_checkStreamNamespace(self):
     622        """
     623        The stream namespace must match the pre-defined stream namespace.
     624        """
     625        xs = self.xmlstream
     626        xs.makeConnection(proto_helpers.StringTransport())
     627        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     628                         "xmlns:stream='http://etherx.jabber.org/streams' "
     629                         "from='example.com' to='example.org' id='12345' "
     630                         "version='1.0'>")
     631
     632        self.xmlstream.assertStreamError(self, condition='invalid-namespace')
     633
     634
     635
    303636class PrepareIDNNameTests(unittest.TestCase):
    304637    """
    305638    Tests for L{wokkel.generic.prepareIDNName}.
Note: See TracBrowser for help on using the repository browser.