~emesene-team/emesene/master

« back to all changes in this revision

Viewing changes to emesene/e3/xmpp/SleekXMPP/sleekxmpp/test/sleektest.py

  • Committer: Riccardo (C10uD)
  • Date: 2012-07-01 16:54:34 UTC
  • Revision ID: git-v1:17ed298a3acb830f76aa2703351993cff749ed35
import2

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""
 
2
    SleekXMPP: The Sleek XMPP Library
 
3
    Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
 
4
    This file is part of SleekXMPP.
 
5
 
 
6
    See the file LICENSE for copying permission.
 
7
"""
 
8
 
 
9
import unittest
 
10
from xml.parsers.expat import ExpatError
 
11
try:
 
12
    import Queue as queue
 
13
except:
 
14
    import queue
 
15
 
 
16
import sleekxmpp
 
17
from sleekxmpp import ClientXMPP, ComponentXMPP
 
18
from sleekxmpp.stanza import Message, Iq, Presence
 
19
from sleekxmpp.test import TestSocket, TestLiveSocket
 
20
from sleekxmpp.exceptions import XMPPError, IqTimeout, IqError
 
21
from sleekxmpp.xmlstream import ET, register_stanza_plugin
 
22
from sleekxmpp.xmlstream import ElementBase, StanzaBase
 
23
from sleekxmpp.xmlstream.tostring import tostring
 
24
from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId
 
25
from sleekxmpp.xmlstream.matcher import MatchXMLMask, MatchXPath
 
26
 
 
27
 
 
28
class SleekTest(unittest.TestCase):
 
29
 
 
30
    """
 
31
    A SleekXMPP specific TestCase class that provides
 
32
    methods for comparing message, iq, and presence stanzas.
 
33
 
 
34
    Methods:
 
35
        Message              -- Create a Message stanza object.
 
36
        Iq                   -- Create an Iq stanza object.
 
37
        Presence             -- Create a Presence stanza object.
 
38
        check_jid            -- Check a JID and its component parts.
 
39
        check                -- Compare a stanza against an XML string.
 
40
        stream_start         -- Initialize a dummy XMPP client.
 
41
        stream_close         -- Disconnect the XMPP client.
 
42
        make_header          -- Create a stream header.
 
43
        send_header          -- Check that the given header has been sent.
 
44
        send_feature         -- Send a raw XML element.
 
45
        send                 -- Check that the XMPP client sent the given
 
46
                                generic stanza.
 
47
        recv                 -- Queue data for XMPP client to receive, or
 
48
                                verify the data that was received from a
 
49
                                live connection.
 
50
        recv_header          -- Check that a given stream header
 
51
                                was received.
 
52
        recv_feature         -- Check that a given, raw XML element
 
53
                                was recveived.
 
54
        fix_namespaces       -- Add top-level namespace to an XML object.
 
55
        compare              -- Compare XML objects against each other.
 
56
    """
 
57
 
 
58
    def __init__(self, *args, **kwargs):
 
59
        unittest.TestCase.__init__(self, *args, **kwargs)
 
60
        self.xmpp = None
 
61
 
 
62
    def parse_xml(self, xml_string):
 
63
        try:
 
64
            xml = ET.fromstring(xml_string)
 
65
            return xml
 
66
        except (SyntaxError, ExpatError) as e:
 
67
            msg = e.msg if hasattr(e, 'msg') else e.message
 
68
            if 'unbound' in msg:
 
69
                known_prefixes = {
 
70
                        'stream': 'http://etherx.jabber.org/streams'}
 
71
 
 
72
                prefix = xml_string.split('<')[1].split(':')[0]
 
73
                if prefix in known_prefixes:
 
74
                    xml_string = '<fixns xmlns:%s="%s">%s</fixns>' % (
 
75
                            prefix,
 
76
                            known_prefixes[prefix],
 
77
                            xml_string)
 
78
                xml = self.parse_xml(xml_string)
 
79
                xml = list(xml)[0]
 
80
                return xml
 
81
            else:
 
82
                self.fail("XML data was mal-formed:\n%s" % xml_string)
 
83
 
 
84
    # ------------------------------------------------------------------
 
85
    # Shortcut methods for creating stanza objects
 
86
 
 
87
    def Message(self, *args, **kwargs):
 
88
        """
 
89
        Create a Message stanza.
 
90
 
 
91
        Uses same arguments as StanzaBase.__init__
 
92
 
 
93
        Arguments:
 
94
            xml -- An XML object to use for the Message's values.
 
95
        """
 
96
        return Message(self.xmpp, *args, **kwargs)
 
97
 
 
98
    def Iq(self, *args, **kwargs):
 
99
        """
 
100
        Create an Iq stanza.
 
101
 
 
102
        Uses same arguments as StanzaBase.__init__
 
103
 
 
104
        Arguments:
 
105
            xml -- An XML object to use for the Iq's values.
 
106
        """
 
107
        return Iq(self.xmpp, *args, **kwargs)
 
108
 
 
109
    def Presence(self, *args, **kwargs):
 
110
        """
 
111
        Create a Presence stanza.
 
112
 
 
113
        Uses same arguments as StanzaBase.__init__
 
114
 
 
115
        Arguments:
 
116
            xml -- An XML object to use for the Iq's values.
 
117
        """
 
118
        return Presence(self.xmpp, *args, **kwargs)
 
119
 
 
120
    def check_jid(self, jid, user=None, domain=None, resource=None,
 
121
                  bare=None, full=None, string=None):
 
122
        """
 
123
        Verify the components of a JID.
 
124
 
 
125
        Arguments:
 
126
            jid      -- The JID object to test.
 
127
            user     -- Optional. The user name portion of the JID.
 
128
            domain   -- Optional. The domain name portion of the JID.
 
129
            resource -- Optional. The resource portion of the JID.
 
130
            bare     -- Optional. The bare JID.
 
131
            full     -- Optional. The full JID.
 
132
            string   -- Optional. The string version of the JID.
 
133
        """
 
134
        if user is not None:
 
135
            self.assertEqual(jid.user, user,
 
136
                    "User does not match: %s" % jid.user)
 
137
        if domain is not None:
 
138
            self.assertEqual(jid.domain, domain,
 
139
                    "Domain does not match: %s" % jid.domain)
 
140
        if resource is not None:
 
141
            self.assertEqual(jid.resource, resource,
 
142
                    "Resource does not match: %s" % jid.resource)
 
143
        if bare is not None:
 
144
            self.assertEqual(jid.bare, bare,
 
145
                    "Bare JID does not match: %s" % jid.bare)
 
146
        if full is not None:
 
147
            self.assertEqual(jid.full, full,
 
148
                    "Full JID does not match: %s" % jid.full)
 
149
        if string is not None:
 
150
            self.assertEqual(str(jid), string,
 
151
                    "String does not match: %s" % str(jid))
 
152
 
 
153
    def check_roster(self, owner, jid, name=None, subscription=None,
 
154
                     afrom=None, ato=None, pending_out=None, pending_in=None,
 
155
                     groups=None):
 
156
        roster = self.xmpp.roster[owner][jid]
 
157
        if name is not None:
 
158
            self.assertEqual(roster['name'], name,
 
159
                    "Incorrect name value: %s" % roster['name'])
 
160
        if subscription is not None:
 
161
            self.assertEqual(roster['subscription'], subscription,
 
162
                    "Incorrect subscription: %s" % roster['subscription'])
 
163
        if afrom is not None:
 
164
            self.assertEqual(roster['from'], afrom,
 
165
                    "Incorrect from state: %s" % roster['from'])
 
166
        if ato is not None:
 
167
            self.assertEqual(roster['to'], ato,
 
168
                    "Incorrect to state: %s" % roster['to'])
 
169
        if pending_out is not None:
 
170
            self.assertEqual(roster['pending_out'], pending_out,
 
171
                    "Incorrect pending_out state: %s" % roster['pending_out'])
 
172
        if pending_in is not None:
 
173
            self.assertEqual(roster['pending_in'], pending_out,
 
174
                    "Incorrect pending_in state: %s" % roster['pending_in'])
 
175
        if groups is not None:
 
176
            self.assertEqual(roster['groups'], groups,
 
177
                    "Incorrect groups: %s" % roster['groups'])
 
178
 
 
179
    # ------------------------------------------------------------------
 
180
    # Methods for comparing stanza objects to XML strings
 
181
 
 
182
    def check(self, stanza, criteria, method='exact',
 
183
              defaults=None, use_values=True):
 
184
        """
 
185
        Create and compare several stanza objects to a correct XML string.
 
186
 
 
187
        If use_values is False, tests using stanza.values will not be used.
 
188
 
 
189
        Some stanzas provide default values for some interfaces, but
 
190
        these defaults can be problematic for testing since they can easily
 
191
        be forgotten when supplying the XML string. A list of interfaces that
 
192
        use defaults may be provided and the generated stanzas will use the
 
193
        default values for those interfaces if needed.
 
194
 
 
195
        However, correcting the supplied XML is not possible for interfaces
 
196
        that add or remove XML elements. Only interfaces that map to XML
 
197
        attributes may be set using the defaults parameter. The supplied XML
 
198
        must take into account any extra elements that are included by default.
 
199
 
 
200
        Arguments:
 
201
            stanza       -- The stanza object to test.
 
202
            criteria     -- An expression the stanza must match against.
 
203
            method       -- The type of matching to use; one of:
 
204
                            'exact', 'mask', 'id', 'xpath', and 'stanzapath'.
 
205
                            Defaults to the value of self.match_method.
 
206
            defaults     -- A list of stanza interfaces that have default
 
207
                            values. These interfaces will be set to their
 
208
                            defaults for the given and generated stanzas to
 
209
                            prevent unexpected test failures.
 
210
            use_values   -- Indicates if testing using stanza.values should
 
211
                            be used. Defaults to True.
 
212
        """
 
213
        if method is None and hasattr(self, 'match_method'):
 
214
            method = getattr(self, 'match_method')
 
215
 
 
216
        if method != 'exact':
 
217
            matchers = {'stanzapath': StanzaPath,
 
218
                        'xpath': MatchXPath,
 
219
                        'mask': MatchXMLMask,
 
220
                        'id': MatcherId}
 
221
            Matcher = matchers.get(method, None)
 
222
            if Matcher is None:
 
223
                raise ValueError("Unknown matching method.")
 
224
            test = Matcher(criteria)
 
225
            self.failUnless(test.match(stanza),
 
226
                    "Stanza did not match using %s method:\n" % method + \
 
227
                    "Criteria:\n%s\n" % str(criteria) + \
 
228
                    "Stanza:\n%s" % str(stanza))
 
229
        else:
 
230
            stanza_class = stanza.__class__
 
231
            if not isinstance(criteria, ElementBase):
 
232
                xml = self.parse_xml(criteria)
 
233
            else:
 
234
                xml = criteria.xml
 
235
 
 
236
            # Ensure that top level namespaces are used, even if they
 
237
            # were not provided.
 
238
            self.fix_namespaces(stanza.xml, 'jabber:client')
 
239
            self.fix_namespaces(xml, 'jabber:client')
 
240
 
 
241
            stanza2 = stanza_class(xml=xml)
 
242
 
 
243
            if use_values:
 
244
                # Using stanza.values will add XML for any interface that
 
245
                # has a default value. We need to set those defaults on
 
246
                # the existing stanzas and XML so that they will compare
 
247
                # correctly.
 
248
                default_stanza = stanza_class()
 
249
                if defaults is None:
 
250
                    known_defaults = {
 
251
                        Message: ['type'],
 
252
                        Presence: ['priority']
 
253
                    }
 
254
                    defaults = known_defaults.get(stanza_class, [])
 
255
                for interface in defaults:
 
256
                    stanza[interface] = stanza[interface]
 
257
                    stanza2[interface] = stanza2[interface]
 
258
                    # Can really only automatically add defaults for top
 
259
                    # level attribute values. Anything else must be accounted
 
260
                    # for in the provided XML string.
 
261
                    if interface not in xml.attrib:
 
262
                        if interface in default_stanza.xml.attrib:
 
263
                            value = default_stanza.xml.attrib[interface]
 
264
                            xml.attrib[interface] = value
 
265
 
 
266
                values = stanza2.values
 
267
                stanza3 = stanza_class()
 
268
                stanza3.values = values
 
269
 
 
270
                debug = "Three methods for creating stanzas do not match.\n"
 
271
                debug += "Given XML:\n%s\n" % tostring(xml)
 
272
                debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
 
273
                debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
 
274
                debug += "Second generated stanza:\n%s\n" % tostring(stanza3.xml)
 
275
                result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml)
 
276
            else:
 
277
                debug = "Two methods for creating stanzas do not match.\n"
 
278
                debug += "Given XML:\n%s\n" % tostring(xml)
 
279
                debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
 
280
                debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
 
281
                result = self.compare(xml, stanza.xml, stanza2.xml)
 
282
 
 
283
            self.failUnless(result, debug)
 
284
 
 
285
    # ------------------------------------------------------------------
 
286
    # Methods for simulating stanza streams.
 
287
 
 
288
    def stream_disconnect(self):
 
289
        """
 
290
        Simulate a stream disconnection.
 
291
        """
 
292
        if self.xmpp:
 
293
            self.xmpp.socket.disconnect_error()
 
294
 
 
295
    def stream_start(self, mode='client', skip=True, header=None,
 
296
                           socket='mock', jid='tester@localhost',
 
297
                           password='test', server='localhost',
 
298
                           port=5222, sasl_mech=None,
 
299
                           plugins=None, plugin_config={}):
 
300
        """
 
301
        Initialize an XMPP client or component using a dummy XML stream.
 
302
 
 
303
        Arguments:
 
304
            mode     -- Either 'client' or 'component'. Defaults to 'client'.
 
305
            skip     -- Indicates if the first item in the sent queue (the
 
306
                        stream header) should be removed. Tests that wish
 
307
                        to test initializing the stream should set this to
 
308
                        False. Otherwise, the default of True should be used.
 
309
            socket   -- Either 'mock' or 'live' to indicate if the socket
 
310
                        should be a dummy, mock socket or a live, functioning
 
311
                        socket. Defaults to 'mock'.
 
312
            jid      -- The JID to use for the connection.
 
313
                        Defaults to 'tester@localhost'.
 
314
            password -- The password to use for the connection.
 
315
                        Defaults to 'test'.
 
316
            server   -- The name of the XMPP server. Defaults to 'localhost'.
 
317
            port     -- The port to use when connecting to the server.
 
318
                        Defaults to 5222.
 
319
            plugins  -- List of plugins to register. By default, all plugins
 
320
                        are loaded.
 
321
        """
 
322
        if mode == 'client':
 
323
            self.xmpp = ClientXMPP(jid, password,
 
324
                                   sasl_mech=sasl_mech,
 
325
                                   plugin_config=plugin_config)
 
326
        elif mode == 'component':
 
327
            self.xmpp = ComponentXMPP(jid, password,
 
328
                                      server, port,
 
329
                                      plugin_config=plugin_config)
 
330
        else:
 
331
            raise ValueError("Unknown XMPP connection mode.")
 
332
 
 
333
        # Remove unique ID prefix to make it easier to test
 
334
        self.xmpp._id_prefix = ''
 
335
        self.xmpp._disconnect_wait_for_threads = False
 
336
        self.xmpp.default_lang = None
 
337
        self.xmpp.peer_default_lang = None
 
338
 
 
339
        # We will use this to wait for the session_start event
 
340
        # for live connections.
 
341
        skip_queue = queue.Queue()
 
342
 
 
343
        if socket == 'mock':
 
344
            self.xmpp.set_socket(TestSocket())
 
345
 
 
346
            # Simulate connecting for mock sockets.
 
347
            self.xmpp.auto_reconnect = False
 
348
            self.xmpp.state._set_state('connected')
 
349
 
 
350
            # Must have the stream header ready for xmpp.process() to work.
 
351
            if not header:
 
352
                header = self.xmpp.stream_header
 
353
            self.xmpp.socket.recv_data(header)
 
354
        elif socket == 'live':
 
355
            self.xmpp.socket_class = TestLiveSocket
 
356
 
 
357
            def wait_for_session(x):
 
358
                self.xmpp.socket.clear()
 
359
                skip_queue.put('started')
 
360
 
 
361
            self.xmpp.add_event_handler('session_start', wait_for_session)
 
362
            if server is not None:
 
363
                self.xmpp.connect((server, port))
 
364
            else:
 
365
                self.xmpp.connect()
 
366
        else:
 
367
            raise ValueError("Unknown socket type.")
 
368
 
 
369
        if plugins is None:
 
370
            self.xmpp.register_plugins()
 
371
        else:
 
372
            for plugin in plugins:
 
373
                self.xmpp.register_plugin(plugin)
 
374
        self.xmpp.process(threaded=True)
 
375
        if skip:
 
376
            if socket != 'live':
 
377
                # Mark send queue as usable
 
378
                self.xmpp.session_started_event.set()
 
379
                # Clear startup stanzas
 
380
                self.xmpp.socket.next_sent(timeout=1)
 
381
                if mode == 'component':
 
382
                    self.xmpp.socket.next_sent(timeout=1)
 
383
            else:
 
384
                skip_queue.get(block=True, timeout=10)
 
385
 
 
386
    def make_header(self, sto='',
 
387
                          sfrom='',
 
388
                          sid='',
 
389
                          stream_ns="http://etherx.jabber.org/streams",
 
390
                          default_ns="jabber:client",
 
391
                          default_lang="en",
 
392
                          version="1.0",
 
393
                          xml_header=True):
 
394
        """
 
395
        Create a stream header to be received by the test XMPP agent.
 
396
 
 
397
        The header must be saved and passed to stream_start.
 
398
 
 
399
        Arguments:
 
400
            sto        -- The recipient of the stream header.
 
401
            sfrom      -- The agent sending the stream header.
 
402
            sid        -- The stream's id.
 
403
            stream_ns  -- The namespace of the stream's root element.
 
404
            default_ns -- The default stanza namespace.
 
405
            version    -- The stream version.
 
406
            xml_header -- Indicates if the XML version header should be
 
407
                          appended before the stream header.
 
408
        """
 
409
        header = '<stream:stream %s>'
 
410
        parts = []
 
411
        if xml_header:
 
412
            header = '<?xml version="1.0"?>' + header
 
413
        if sto:
 
414
            parts.append('to="%s"' % sto)
 
415
        if sfrom:
 
416
            parts.append('from="%s"' % sfrom)
 
417
        if sid:
 
418
            parts.append('id="%s"' % sid)
 
419
        if default_lang:
 
420
            parts.append('xml:lang="%s"' % default_lang)
 
421
        parts.append('version="%s"' % version)
 
422
        parts.append('xmlns:stream="%s"' % stream_ns)
 
423
        parts.append('xmlns="%s"' % default_ns)
 
424
        return header % ' '.join(parts)
 
425
 
 
426
    def recv(self, data, defaults=[], method='exact',
 
427
             use_values=True, timeout=1):
 
428
        """
 
429
        Pass data to the dummy XMPP client as if it came from an XMPP server.
 
430
 
 
431
        If using a live connection, verify what the server has sent.
 
432
 
 
433
        Arguments:
 
434
            data         -- If a dummy socket is being used, the XML that is to
 
435
                            be received next. Otherwise it is the criteria used
 
436
                            to match against live data that is received.
 
437
            defaults     -- A list of stanza interfaces with default values that
 
438
                            may interfere with comparisons.
 
439
            method       -- Select the type of comparison to use for
 
440
                            verifying the received stanza. Options are 'exact',
 
441
                            'id', 'stanzapath', 'xpath', and 'mask'.
 
442
                            Defaults to the value of self.match_method.
 
443
            use_values   -- Indicates if stanza comparisons should test using
 
444
                            stanza.values. Defaults to True.
 
445
            timeout      -- Time to wait in seconds for data to be received by
 
446
                            a live connection.
 
447
        """
 
448
        if self.xmpp.socket.is_live:
 
449
            # we are working with a live connection, so we should
 
450
            # verify what has been received instead of simulating
 
451
            # receiving data.
 
452
            recv_data = self.xmpp.socket.next_recv(timeout)
 
453
            if recv_data is None:
 
454
                self.fail("No stanza was received.")
 
455
            xml = self.parse_xml(recv_data)
 
456
            self.fix_namespaces(xml, 'jabber:client')
 
457
            stanza = self.xmpp._build_stanza(xml, 'jabber:client')
 
458
            self.check(stanza, data,
 
459
                       method=method,
 
460
                       defaults=defaults,
 
461
                       use_values=use_values)
 
462
        else:
 
463
            # place the data in the dummy socket receiving queue.
 
464
            data = str(data)
 
465
            self.xmpp.socket.recv_data(data)
 
466
 
 
467
    def recv_header(self, sto='',
 
468
                          sfrom='',
 
469
                          sid='',
 
470
                          stream_ns="http://etherx.jabber.org/streams",
 
471
                          default_ns="jabber:client",
 
472
                          version="1.0",
 
473
                          xml_header=False,
 
474
                          timeout=1):
 
475
        """
 
476
        Check that a given stream header was received.
 
477
 
 
478
        Arguments:
 
479
            sto        -- The recipient of the stream header.
 
480
            sfrom      -- The agent sending the stream header.
 
481
            sid        -- The stream's id. Set to None to ignore.
 
482
            stream_ns  -- The namespace of the stream's root element.
 
483
            default_ns -- The default stanza namespace.
 
484
            version    -- The stream version.
 
485
            xml_header -- Indicates if the XML version header should be
 
486
                          appended before the stream header.
 
487
            timeout    -- Length of time to wait in seconds for a
 
488
                          response.
 
489
        """
 
490
        header = self.make_header(sto, sfrom, sid,
 
491
                                  stream_ns=stream_ns,
 
492
                                  default_ns=default_ns,
 
493
                                  version=version,
 
494
                                  xml_header=xml_header)
 
495
        recv_header = self.xmpp.socket.next_recv(timeout)
 
496
        if recv_header is None:
 
497
            raise ValueError("Socket did not return data.")
 
498
 
 
499
        # Apply closing elements so that we can construct
 
500
        # XML objects for comparison.
 
501
        header2 = header + '</stream:stream>'
 
502
        recv_header2 = recv_header + '</stream:stream>'
 
503
 
 
504
        xml = self.parse_xml(header2)
 
505
        recv_xml = self.parse_xml(recv_header2)
 
506
 
 
507
        if sid is None:
 
508
            # Ignore the id sent by the server since
 
509
            # we can't know in advance what it will be.
 
510
            if 'id' in recv_xml.attrib:
 
511
                del recv_xml.attrib['id']
 
512
 
 
513
        # Ignore the xml:lang attribute for now.
 
514
        if 'xml:lang' in recv_xml.attrib:
 
515
            del recv_xml.attrib['xml:lang']
 
516
        xml_ns = 'http://www.w3.org/XML/1998/namespace'
 
517
        if '{%s}lang' % xml_ns in recv_xml.attrib:
 
518
            del recv_xml.attrib['{%s}lang' % xml_ns]
 
519
 
 
520
        if list(recv_xml):
 
521
            # We received more than just the header
 
522
            for xml in recv_xml:
 
523
                self.xmpp.socket.recv_data(tostring(xml))
 
524
 
 
525
            attrib = recv_xml.attrib
 
526
            recv_xml.clear()
 
527
            recv_xml.attrib = attrib
 
528
 
 
529
        self.failUnless(
 
530
            self.compare(xml, recv_xml),
 
531
            "Stream headers do not match:\nDesired:\n%s\nReceived:\n%s" % (
 
532
                '%s %s' % (xml.tag, xml.attrib),
 
533
                '%s %s' % (recv_xml.tag, recv_xml.attrib)))
 
534
 
 
535
    def recv_feature(self, data, method='mask', use_values=True, timeout=1):
 
536
        """
 
537
        """
 
538
        if method is None and hasattr(self, 'match_method'):
 
539
            method = getattr(self, 'match_method')
 
540
 
 
541
        if self.xmpp.socket.is_live:
 
542
            # we are working with a live connection, so we should
 
543
            # verify what has been received instead of simulating
 
544
            # receiving data.
 
545
            recv_data = self.xmpp.socket.next_recv(timeout)
 
546
            xml = self.parse_xml(data)
 
547
            recv_xml = self.parse_xml(recv_data)
 
548
            if recv_data is None:
 
549
                self.fail("No stanza was received.")
 
550
            if method == 'exact':
 
551
                self.failUnless(self.compare(xml, recv_xml),
 
552
                    "Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
 
553
                        tostring(xml), tostring(recv_xml)))
 
554
            elif method == 'mask':
 
555
                matcher = MatchXMLMask(xml)
 
556
                self.failUnless(matcher.match(recv_xml),
 
557
                    "Stanza did not match using %s method:\n" % method + \
 
558
                    "Criteria:\n%s\n" % tostring(xml) + \
 
559
                    "Stanza:\n%s" % tostring(recv_xml))
 
560
            else:
 
561
                raise ValueError("Uknown matching method: %s" % method)
 
562
        else:
 
563
            # place the data in the dummy socket receiving queue.
 
564
            data = str(data)
 
565
            self.xmpp.socket.recv_data(data)
 
566
 
 
567
    def send_header(self, sto='',
 
568
                          sfrom='',
 
569
                          sid='',
 
570
                          stream_ns="http://etherx.jabber.org/streams",
 
571
                          default_ns="jabber:client",
 
572
                          default_lang="en",
 
573
                          version="1.0",
 
574
                          xml_header=False,
 
575
                          timeout=1):
 
576
        """
 
577
        Check that a given stream header was sent.
 
578
 
 
579
        Arguments:
 
580
            sto        -- The recipient of the stream header.
 
581
            sfrom      -- The agent sending the stream header.
 
582
            sid        -- The stream's id.
 
583
            stream_ns  -- The namespace of the stream's root element.
 
584
            default_ns -- The default stanza namespace.
 
585
            version    -- The stream version.
 
586
            xml_header -- Indicates if the XML version header should be
 
587
                          appended before the stream header.
 
588
            timeout    -- Length of time to wait in seconds for a
 
589
                          response.
 
590
        """
 
591
        header = self.make_header(sto, sfrom, sid,
 
592
                                  stream_ns=stream_ns,
 
593
                                  default_ns=default_ns,
 
594
                                  default_lang=default_lang,
 
595
                                  version=version,
 
596
                                  xml_header=xml_header)
 
597
        sent_header = self.xmpp.socket.next_sent(timeout)
 
598
        if sent_header is None:
 
599
            raise ValueError("Socket did not return data.")
 
600
 
 
601
        # Apply closing elements so that we can construct
 
602
        # XML objects for comparison.
 
603
        header2 = header + '</stream:stream>'
 
604
        sent_header2 = sent_header + b'</stream:stream>'
 
605
 
 
606
        xml = self.parse_xml(header2)
 
607
        sent_xml = self.parse_xml(sent_header2)
 
608
 
 
609
        self.failUnless(
 
610
            self.compare(xml, sent_xml),
 
611
            "Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % (
 
612
                header, sent_header))
 
613
 
 
614
    def send_feature(self, data, method='mask', use_values=True, timeout=1):
 
615
        """
 
616
        """
 
617
        sent_data = self.xmpp.socket.next_sent(timeout)
 
618
        xml = self.parse_xml(data)
 
619
        sent_xml = self.parse_xml(sent_data)
 
620
        if sent_data is None:
 
621
            self.fail("No stanza was sent.")
 
622
        if method == 'exact':
 
623
            self.failUnless(self.compare(xml, sent_xml),
 
624
                "Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
 
625
                    tostring(xml), tostring(sent_xml)))
 
626
        elif method == 'mask':
 
627
            matcher = MatchXMLMask(xml)
 
628
            self.failUnless(matcher.match(sent_xml),
 
629
                "Stanza did not match using %s method:\n" % method + \
 
630
                "Criteria:\n%s\n" % tostring(xml) + \
 
631
                "Stanza:\n%s" % tostring(sent_xml))
 
632
        else:
 
633
            raise ValueError("Uknown matching method: %s" % method)
 
634
 
 
635
    def send(self, data, defaults=None, use_values=True,
 
636
             timeout=.5, method='exact'):
 
637
        """
 
638
        Check that the XMPP client sent the given stanza XML.
 
639
 
 
640
        Extracts the next sent stanza and compares it with the given
 
641
        XML using check.
 
642
 
 
643
        Arguments:
 
644
            stanza_class -- The class of the sent stanza object.
 
645
            data         -- The XML string of the expected Message stanza,
 
646
                            or an equivalent stanza object.
 
647
            use_values   -- Modifies the type of tests used by check_message.
 
648
            defaults     -- A list of stanza interfaces that have defaults
 
649
                            values which may interfere with comparisons.
 
650
            timeout      -- Time in seconds to wait for a stanza before
 
651
                            failing the check.
 
652
            method       -- Select the type of comparison to use for
 
653
                            verifying the sent stanza. Options are 'exact',
 
654
                            'id', 'stanzapath', 'xpath', and 'mask'.
 
655
                            Defaults to the value of self.match_method.
 
656
        """
 
657
        sent = self.xmpp.socket.next_sent(timeout)
 
658
        if data is None and sent is None:
 
659
            return
 
660
        if data is None and sent is not None:
 
661
            self.fail("Stanza data was sent: %s" % sent)
 
662
        if sent is None:
 
663
            self.fail("No stanza was sent.")
 
664
 
 
665
        xml = self.parse_xml(sent)
 
666
        self.fix_namespaces(xml, 'jabber:client')
 
667
        sent = self.xmpp._build_stanza(xml, 'jabber:client')
 
668
        self.check(sent, data,
 
669
                   method=method,
 
670
                   defaults=defaults,
 
671
                   use_values=use_values)
 
672
 
 
673
    def stream_close(self):
 
674
        """
 
675
        Disconnect the dummy XMPP client.
 
676
 
 
677
        Can be safely called even if stream_start has not been called.
 
678
 
 
679
        Must be placed in the tearDown method of a test class to ensure
 
680
        that the XMPP client is disconnected after an error.
 
681
        """
 
682
        if hasattr(self, 'xmpp') and self.xmpp is not None:
 
683
            self.xmpp.socket.recv_data(self.xmpp.stream_footer)
 
684
            self.xmpp.disconnect()
 
685
 
 
686
    # ------------------------------------------------------------------
 
687
    # XML Comparison and Cleanup
 
688
 
 
689
    def fix_namespaces(self, xml, ns):
 
690
        """
 
691
        Assign a namespace to an element and any children that
 
692
        don't have a namespace.
 
693
 
 
694
        Arguments:
 
695
            xml -- The XML object to fix.
 
696
            ns  -- The namespace to add to the XML object.
 
697
        """
 
698
        if xml.tag.startswith('{'):
 
699
            return
 
700
        xml.tag = '{%s}%s' % (ns, xml.tag)
 
701
        for child in xml:
 
702
            self.fix_namespaces(child, ns)
 
703
 
 
704
    def compare(self, xml, *other):
 
705
        """
 
706
        Compare XML objects.
 
707
 
 
708
        Arguments:
 
709
            xml    -- The XML object to compare against.
 
710
            *other -- The list of XML objects to compare.
 
711
        """
 
712
        if not other:
 
713
            return False
 
714
 
 
715
        # Compare multiple objects
 
716
        if len(other) > 1:
 
717
            for xml2 in other:
 
718
                if not self.compare(xml, xml2):
 
719
                    return False
 
720
            return True
 
721
 
 
722
        other = other[0]
 
723
 
 
724
        # Step 1: Check tags
 
725
        if xml.tag != other.tag:
 
726
            return False
 
727
 
 
728
        # Step 2: Check attributes
 
729
        if xml.attrib != other.attrib:
 
730
            return False
 
731
 
 
732
        # Step 3: Check text
 
733
        if xml.text is None:
 
734
            xml.text = ""
 
735
        if other.text is None:
 
736
            other.text = ""
 
737
        xml.text = xml.text.strip()
 
738
        other.text = other.text.strip()
 
739
 
 
740
        if xml.text != other.text:
 
741
            return False
 
742
 
 
743
        # Step 4: Check children count
 
744
        if len(list(xml)) != len(list(other)):
 
745
            return False
 
746
 
 
747
        # Step 5: Recursively check children
 
748
        for child in xml:
 
749
            child2s = other.findall("%s" % child.tag)
 
750
            if child2s is None:
 
751
                return False
 
752
            for child2 in child2s:
 
753
                if self.compare(child, child2):
 
754
                    break
 
755
            else:
 
756
                return False
 
757
 
 
758
        # Step 6: Recursively check children the other way.
 
759
        for child in other:
 
760
            child2s = xml.findall("%s" % child.tag)
 
761
            if child2s is None:
 
762
                return False
 
763
            for child2 in child2s:
 
764
                if self.compare(child, child2):
 
765
                    break
 
766
            else:
 
767
                return False
 
768
 
 
769
        # Everything matches
 
770
        return True