source: ralphm-patches/listening-authenticator-stream-features.patch @ 72:727b4d29c48e

Last change on this file since 72:727b4d29c48e was 72:727b4d29c48e, checked in by Ralph Meijer <ralphm@…>, 8 years ago

Major reworking of avatars, session manager and stanza handlers.

File size: 24.7 KB
  • wokkel/generic.py

    # HG changeset patch
    # Parent 840b96390047670c5209195300f902689c18b12f
    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  
    1010from zope.interface import implements
    1111
    1212from twisted.internet import defer, protocol
    13 from twisted.python import reflect
     13from twisted.python import log, reflect
    1414from twisted.python.deprecate import deprecated
    1515from twisted.python.versions import Version
    1616from twisted.words.protocols.jabber import error, jid, xmlstream
     
    1818from twisted.words.xish import domish, utility
    1919from twisted.words.xish.xmlstream import BootstrapMixin
    2020
    21 from wokkel.iwokkel import IDisco
     21from wokkel.iwokkel import IDisco, IReceivingInitializer
    2222from wokkel.subprotocols import XMPPHandler
    2323
    2424IQ_GET = '/iq[@type="get"]'
     
    2727NS_VERSION = 'jabber:iq:version'
    2828VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]'
    2929
     30XPATH_ALL = "/*"
     31
    3032def parseXml(string):
    3133    """
    3234    Parse serialized XML into a DOM structure.
     
    332334
    333335
    334336
     337class TestableXmlStream(xmlstream.XmlStream):
     338    """
     339    XML Stream that buffers outgoing data and catches special events.
     340
     341    This implementation overrides relevant methods to prevent any data
     342    to be sent out on a transport. Instead it buffers all outgoing stanzas,
     343    sets flags instead of sending the stream header and footer and logs stream
     344    errors so it can be caught by a logging observer.
     345
     346    @ivar output: Sequence of objects sent out using L{send}. Usually these are
     347        L{domish.Element} instances.
     348    @type output: C{list}
     349
     350    @ivar headerSent: Flag set when a stream header would have been sent. When
     351        a stream restart occurs through L{reset}, this flag is reset as well.
     352    @type headerSent: C{bool}
     353
     354    @ivar footerSent: Flag set when a stream footer would have been sent
     355        explicitly. Note that it is not set when a stream error is sent
     356        using L{sendStreamError}.
     357    @type footerSent: C{bool}
     358    """
     359
     360
     361    def __init__(self, authenticator):
     362        xmlstream.XmlStream.__init__(self, authenticator)
     363        self.headerSent = False
     364        self.footerSent = False
     365        self.output = []
     366
     367
     368    def reset(self):
     369        xmlstream.XmlStream.reset(self)
     370        self.headerSent = False
     371
     372
     373    def sendHeader(self):
     374        self.headerSent = True
     375
     376
     377    def sendFooter(self):
     378        self.footerSent = True
     379
     380
     381    def sendStreamError(self, streamError):
     382        """
     383        Log a stream error.
     384
     385        If this is called from a Twisted Trial test case, the stream error
     386        will be observed by the Trial logging observer. If it is not explicitly
     387        tested for (i.e. flushed), this will cause the test case to
     388        automatically fail. See L{assertStreamError} for a convenience method
     389        to test for stream errors.
     390
     391        @type streamError: L{error.StreamError}
     392        """
     393        log.err(streamError)
     394
     395
     396    @staticmethod
     397    def assertStreamError(testcase, condition=None, exc=None):
     398        """
     399        Check if a stream error was sent out.
     400
     401        To check for stream errors sent out by L{sendStreamError}, this method
     402        will flush logged stream errors and inspect the last one. If
     403        C{condition} was passed, the logged error is asserted to match that
     404        condition. If C{exc} was passed, the logged error is asserted to be
     405        identical to it.
     406
     407        Note that this is takes the calling test case as the first argument, to
     408        be able to hook into its methods for flushing errors and making
     409        assertions.
     410
     411        @param testcase: The test case instance that is calling this method.
     412        @type testcase: {twisted.trial.unittest.TestCase}
     413
     414        @param condition: The optional stream error condition to match against.
     415        @type condition: C{unicode}.
     416
     417        @param exc: The optional stream error to check identity against.
     418        @type exc: L{error.StreamError}
     419        """
     420
     421        loggedErrors = testcase.flushLoggedErrors(error.StreamError)
     422        testcase.assertTrue(loggedErrors, "No stream error was sent")
     423        streamError = loggedErrors[-1].value
     424        if condition:
     425            testcase.assertEqual(condition, streamError.condition)
     426        elif exc:
     427            testcase.assertIdentical(exc, streamError)
     428
     429
     430    def send(self, obj):
     431        """
     432        Buffer all outgoing stanzas.
     433
     434        @type obj: L{domish.Element}
     435        """
     436        self.output.append(obj)
     437
     438
     439
     440class BaseReceivingInitializer(object):
     441    """
     442    Base stream initializer for receiving entities.
     443    """
     444    implements(IReceivingInitializer)
     445
     446    required = False
     447
     448    def __init__(self, name, xs):
     449        self.name = name
     450        self.xmlstream = xs
     451        self.deferred = defer.Deferred()
     452
     453
     454    def getFeatures(self):
     455        raise NotImplementedError()
     456
     457
     458    def initialize(self):
     459        return self.deferred
     460
     461
     462
     463class FeatureListenAuthenticator(xmlstream.ListenAuthenticator):
     464    """
     465    Authenticator for receiving entities with support for initializers.
     466    """
     467
     468    def __init__(self):
     469        self.completedInitializers = []
     470
     471
     472    def _onElementFallback(self, element):
     473        """
     474        Fallback observer that rejects XML Stanzas.
     475
     476        This observer is active while stream feature negotiation has not yet
     477        completed.
     478        """
     479        # ignore elements that are not XML Stanzas
     480        if (element.uri not in (self.namespace) or
     481            element.name not in ('iq', 'message', 'presence')):
     482            return
     483
     484        # ignore elements that have already been handled
     485        if element.handled:
     486            return
     487
     488        exc = error.StreamError('not-authorized')
     489        self.xmlstream.sendStreamError(exc)
     490
     491
     492    def connectionMade(self):
     493        """
     494        Called when the connection has been made.
     495
     496        Adds an observer to reject XML Stanzas until stream feature negotiation
     497        has completed.
     498        """
     499        xmlstream.ListenAuthenticator.connectionMade(self)
     500        self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1)
     501
     502
     503    def _cbInit(self, result):
     504        """
     505        Mark the initializer as completed and continue to the next.
     506        """
     507        result, index = result
     508        self.completedInitializers.append(self._initializers[index].name)
     509        del self._initializers[index]
     510
     511        if result is xmlstream.Reset:
     512            # The initializer initiated a stream restart, bail.
     513            return
     514        else:
     515            self._initializeStream()
     516
     517
     518    def _ebInit(self, failure):
     519        """
     520        Called when an initializer raises an exception.
     521
     522        If the exception is a L{error.StreamError} it is sent out, otherwise
     523        the error is logged and a stream error with condition
     524        C{'internal-server-error'} is sent out instead.
     525        """
     526        firstError = failure.value
     527        subFailure = firstError.subFailure
     528        if subFailure.check(error.StreamError):
     529            exc = subFailure.value
     530        else:
     531            log.err(subFailure, index=firstError.index)
     532            exc = error.StreamError('internal-server-error')
     533
     534        self.xmlstream.sendStreamError(exc)
     535
     536
     537    def _initializeStream(self):
     538        """
     539        Initialize the stream.
     540
     541        This walks all initializers to retrieve their features and determine
     542        if there is at least one required initializer. If not, the stream is
     543        ready for the exchange of stanzas. The features are sent out and each
     544        initializer will have its C{initialize} method called.
     545
     546        If negation has completed, L{xmlstream.STREAM_AUTHD_EVENT} is
     547        dispatched and the observer rejecting incoming stanzas is removed.
     548        """
     549        features = domish.Element((xmlstream.NS_STREAMS, 'features'))
     550        ds = []
     551        required = False
     552        for initializer in self._initializers:
     553            required = required or initializer.required
     554            for feature in initializer.getFeatures():
     555                features.addChild(feature)
     556            d = initializer.initialize()
     557            ds.append(d)
     558
     559        self.xmlstream.send(features)
     560
     561        if not required:
     562            # There are no required initializers anymore. This stream is
     563            # now ready for the exchange of stanzas.
     564            self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback)
     565            self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
     566
     567        if ds:
     568            d = defer.DeferredList(ds, fireOnOneCallback=True,
     569                                       fireOnOneErrback=True,
     570                                       consumeErrors=True)
     571            d.addCallbacks(self._cbInit, self._ebInit)
     572
     573
     574    def getInitializers(self):
     575        """
     576        Get the initializers for the current stage of stream negotiation.
     577
     578        This will be called at the start of each stream start to retrieve
     579        the initializers for which the features are advertised and initiated.
     580
     581        @rtype: C{list} of C{IReceivingInitializer} instances.
     582        """
     583        raise NotImplementedError()
     584
     585
     586    def checkStream(self):
     587        """
     588        Check the stream before sending out a stream header and initialization.
     589
     590        This is the place to inspect the stream properties and raise a relevant
     591        L{error.StreamError} if needed.
     592        """
     593        # Check stream namespace
     594        if self.xmlstream.namespace != self.namespace:
     595            self.xmlstream.namespace = self.namespace
     596            raise error.StreamError('invalid-namespace')
     597
     598
     599    def streamStarted(self, rootElement):
     600        """
     601        Called when the stream header has been received.
     602
     603        Check the stream properties through L{checkStream}, send out
     604        a stream header, and retrieve and initialize the stream initializers.
     605        """
     606        xmlstream.ListenAuthenticator.streamStarted(self, rootElement)
     607
     608        try:
     609            self.checkStream()
     610        except error.StreamError, exc:
     611            self.xmlstream.sendStreamError(exc)
     612            return
     613
     614        self.xmlstream.sendHeader()
     615
     616        self._initializers = self.getInitializers()
     617        self._initializeStream()
     618
     619
     620
    335621@deprecated(Version("Wokkel", 0, 8, 0), "unicode.encode('idna')")
    336622def prepareIDNName(name):
    337623    """
  • 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'
     
    276281
    277282
    278283
     284class BaseReceivingInitializerTest(unittest.TestCase):
     285    """
     286    Tests for L{generic.BaseReceivingInitializer}.
     287    """
     288
     289    def setUp(self):
     290        self.init = generic.BaseReceivingInitializer('init', None)
     291
     292
     293    def test_interface(self):
     294        verify.verifyObject(iwokkel.IReceivingInitializer, self.init)
     295
     296
     297    def test_getFeatures(self):
     298        self.assertRaises(NotImplementedError, self.init.getFeatures)
     299
     300
     301    def test_initialize(self):
     302        d = self.init.initialize()
     303        self.init.deferred.callback(None)
     304        return d
     305
     306
     307
     308class TestableReceivingInitializer(generic.BaseReceivingInitializer):
     309    """
     310    Testable initializer for receiving entities.
     311
     312    This initializer advertises support for a stream feature denoted by
     313    C{uri} and C{name}. Its C{deferred} should be fired to complete
     314    initialization for this initializer.
     315
     316    @ivar uri: Namespace of the stream feature.
     317    @ivar localname: Element localname for the stream feature.
     318    """
     319    required = True
     320
     321    def __init__(self, name, xs, uri, localname):
     322        generic.BaseReceivingInitializer.__init__(self, name, xs)
     323        self.uri = uri
     324        self.localname = localname
     325        self.deferred = defer.Deferred()
     326
     327
     328    def getFeatures(self):
     329        return [domish.Element((self.uri, self.localname))]
     330
     331
     332
     333class FeatureListenAuthenticatorTest(unittest.TestCase):
     334    """
     335    Tests for L{generic.FeatureListenAuthenticator}.
     336    """
     337
     338    def setUp(self):
     339        self.gotAuthenticated = False
     340        self.initFailure = None
     341        self.authenticator = generic.FeatureListenAuthenticator()
     342        self.authenticator.namespace = 'jabber:server'
     343        self.xmlstream = generic.TestableXmlStream(self.authenticator)
     344        self.xmlstream.addObserver('//event/stream/authd',
     345                                   self.onAuthenticated)
     346        self.xmlstream.addObserver('//event/xmpp/initfailed',
     347                                   self.onInitFailed)
     348
     349        self.init = TestableReceivingInitializer('init', self.xmlstream,
     350                                                 'testns', 'test')
     351
     352        def getInitializers():
     353            return [self.init]
     354
     355        self.authenticator.getInitializers = getInitializers
     356
     357
     358    def onAuthenticated(self, obj):
     359        self.gotAuthenticated = True
     360
     361
     362    def onInitFailed(self, failure):
     363        self.initFailure = failure
     364
     365
     366    def test_getInitializers(self):
     367        """
     368        Unoverridden getInitializers raises NotImplementedError.
     369        """
     370        authenticator = generic.FeatureListenAuthenticator()
     371        self.assertRaises(
     372            NotImplementedError,
     373            authenticator.getInitializers)
     374
     375
     376    def test_streamStarted(self):
     377        """
     378        Upon stream start, stream initializers are set up.
     379
     380        The method getInitializers will determine the available stream
     381        initializers given the current state of stream initialization.
     382        hen, each of the returned initializers will be called to set
     383        themselves up.
     384        """
     385        xs = self.xmlstream
     386        xs.makeConnection(proto_helpers.StringTransport())
     387        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     388                         "xmlns:stream='http://etherx.jabber.org/streams' "
     389                         "from='example.com' to='example.org' id='12345' "
     390                         "version='1.0'>")
     391
     392        self.assertTrue(xs.headerSent)
     393
     394        # Check if features were sent
     395        features = xs.output[-1]
     396        self.assertEquals(xmlstream.NS_STREAMS, features.uri)
     397        self.assertEquals('features', features.name)
     398        feature = features.elements().next()
     399        self.assertEqual('testns', feature.uri)
     400        self.assertEqual('test', feature.name)
     401
     402        self.assertFalse(self.gotAuthenticated)
     403
     404        self.init.deferred.callback(None)
     405        self.assertTrue(self.gotAuthenticated)
     406
     407
     408    def test_streamStartedStreamError(self):
     409        """
     410        A stream error raised by the initializer is sent out.
     411        """
     412        xs = self.xmlstream
     413        xs.makeConnection(proto_helpers.StringTransport())
     414        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     415                         "xmlns:stream='http://etherx.jabber.org/streams' "
     416                         "from='example.com' to='example.org' id='12345' "
     417                         "version='1.0'>")
     418
     419        self.assertTrue(xs.headerSent)
     420
     421        xs.output = []
     422        exc = error.StreamError('policy-violation')
     423        self.init.deferred.errback(exc)
     424
     425        self.xmlstream.assertStreamError(self, exc=exc)
     426        self.assertFalse(xs.output)
     427        self.assertFalse(self.gotAuthenticated)
     428
     429
     430    def test_streamStartedOtherError(self):
     431        """
     432        Initializer exceptions are logged and yield a internal-server-error.
     433        """
     434        xs = self.xmlstream
     435        xs.makeConnection(proto_helpers.StringTransport())
     436        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     437                         "xmlns:stream='http://etherx.jabber.org/streams' "
     438                         "from='example.com' to='example.org' id='12345' "
     439                         "version='1.0'>")
     440
     441        self.assertTrue(xs.headerSent)
     442
     443        xs.output = []
     444        class Error(Exception):
     445            pass
     446        self.init.deferred.errback(Error())
     447
     448        self.xmlstream.assertStreamError(self, condition='internal-server-error')
     449        self.assertFalse(xs.output)
     450        self.assertFalse(self.gotAuthenticated)
     451        self.assertEqual(1, len(self.flushLoggedErrors(Error)))
     452
     453
     454    def test_streamStartedInitializerCompleted(self):
     455        """
     456        Succesfully finished initializers are recorded.
     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        xs.output = []
     466        self.init.deferred.callback(None)
     467        self.assertEqual(['init'], self.authenticator.completedInitializers)
     468
     469
     470    def test_streamStartedInitializerCompletedFeatures(self):
     471        """
     472        After completing an initializer, stream features are sent again.
     473
     474        In this case, with only one initializer, there are no more features.
     475        """
     476        xs = self.xmlstream
     477        xs.makeConnection(proto_helpers.StringTransport())
     478        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     479                         "xmlns:stream='http://etherx.jabber.org/streams' "
     480                         "from='example.com' to='example.org' id='12345' "
     481                         "version='1.0'>")
     482
     483        xs.output = []
     484        self.init.deferred.callback(None)
     485
     486        self.assertEqual(1, len(xs.output))
     487        features = xs.output[-1]
     488        self.assertEqual('features', features.name)
     489        self.assertEqual(xmlstream.NS_STREAMS, features.uri)
     490        self.assertFalse(features.children)
     491
     492
     493    def test_streamStartedInitializerCompletedReset(self):
     494        """
     495        If an initializer completes with Reset, no features are sent.
     496        """
     497        xs = self.xmlstream
     498        xs.makeConnection(proto_helpers.StringTransport())
     499        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     500                         "xmlns:stream='http://etherx.jabber.org/streams' "
     501                         "from='example.com' to='example.org' id='12345' "
     502                         "version='1.0'>")
     503
     504        xs.output = []
     505        self.init.deferred.callback(xmlstream.Reset)
     506
     507        self.assertEqual(0, len(xs.output))
     508
     509
     510    def test_streamStartedXmlStanzasRejected(self):
     511        """
     512        XML Stanzas may not be sent before feature negotiation has completed.
     513        """
     514        xs = self.xmlstream
     515        xs.makeConnection(proto_helpers.StringTransport())
     516        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     517                         "xmlns:stream='http://etherx.jabber.org/streams' "
     518                         "from='example.com' to='example.org' id='12345' "
     519                         "version='1.0'>")
     520
     521        xs.dataReceived("<iq to='example.org' from='example.com' type='set'>"
     522                        "  <query xmlns='jabber:iq:version'/>"
     523                        "</iq>")
     524
     525        self.xmlstream.assertStreamError(self, condition='not-authorized')
     526
     527
     528    def test_streamStartedCompleteXmlStanzasAllowed(self):
     529        """
     530        XML Stanzas may sent after feature negotiation has completed.
     531        """
     532        xs = self.xmlstream
     533        xs.makeConnection(proto_helpers.StringTransport())
     534        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     535                         "xmlns:stream='http://etherx.jabber.org/streams' "
     536                         "from='example.com' to='example.org' id='12345' "
     537                         "version='1.0'>")
     538
     539        self.init.deferred.callback(None)
     540
     541        xs.dataReceived("<iq to='example.org' from='example.com' type='set'>"
     542                        "  <query xmlns='jabber:iq:version'/>"
     543                        "</iq>")
     544
     545
     546    def test_streamStartedXmlStanzasHandledIgnored(self):
     547        """
     548        XML Stanzas that have already been handled are ignored.
     549        """
     550        xs = self.xmlstream
     551        xs.makeConnection(proto_helpers.StringTransport())
     552        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     553                         "xmlns:stream='http://etherx.jabber.org/streams' "
     554                         "from='example.com' to='example.org' id='12345' "
     555                         "version='1.0'>")
     556
     557        iq = generic.parseXml("<iq to='example.org' from='example.com' type='set'>"
     558                              "  <query xmlns='jabber:iq:version'/>"
     559                              "</iq>")
     560        iq.handled = True
     561        xs.dispatch(iq)
     562
     563
     564    def test_streamStartedNonXmlStanzasIgnored(self):
     565        """
     566        Elements that are not XML Stranzas are not rejected.
     567        """
     568        xs = self.xmlstream
     569        xs.makeConnection(proto_helpers.StringTransport())
     570        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     571                         "xmlns:stream='http://etherx.jabber.org/streams' "
     572                         "from='example.com' to='example.org' id='12345' "
     573                         "version='1.0'>")
     574
     575        xs.dataReceived("<test xmlns='myns'/>")
     576
     577
     578    def test_streamStartedCheckStream(self):
     579        """
     580        Stream errors raised by checkStream are sent out.
     581        """
     582        def checkStream():
     583            raise error.StreamError('undefined-condition')
     584
     585        self.authenticator.checkStream = checkStream
     586        xs = self.xmlstream
     587        xs.makeConnection(proto_helpers.StringTransport())
     588        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     589                         "xmlns:stream='http://etherx.jabber.org/streams' "
     590                         "from='example.com' to='example.org' id='12345' "
     591                         "version='1.0'>")
     592
     593        self.xmlstream.assertStreamError(self, condition='undefined-condition')
     594        self.assertFalse(xs.headerSent)
     595
     596
     597    def test_checkStreamNamespace(self):
     598        """
     599        The stream namespace must match the pre-defined stream namespace.
     600        """
     601        xs = self.xmlstream
     602        xs.makeConnection(proto_helpers.StringTransport())
     603        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     604                         "xmlns:stream='http://etherx.jabber.org/streams' "
     605                         "from='example.com' to='example.org' id='12345' "
     606                         "version='1.0'>")
     607
     608        self.xmlstream.assertStreamError(self, condition='invalid-namespace')
     609
     610
     611
    279612class PrepareIDNNameTests(unittest.TestCase):
    280613    """
    281614    Tests for L{wokkel.generic.prepareIDNName}.
Note: See TracBrowser for help on using the repository browser.