2
SleekXMPP: The Sleek XMPP Library
3
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
4
This file is part of SleekXMPP.
6
See the file LICENSE for copying permission.
10
from xml.parsers.expat import ExpatError
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
28
class SleekTest(unittest.TestCase):
31
A SleekXMPP specific TestCase class that provides
32
methods for comparing message, iq, and presence stanzas.
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
47
recv -- Queue data for XMPP client to receive, or
48
verify the data that was received from a
50
recv_header -- Check that a given stream header
52
recv_feature -- Check that a given, raw XML element
54
fix_namespaces -- Add top-level namespace to an XML object.
55
compare -- Compare XML objects against each other.
58
def __init__(self, *args, **kwargs):
59
unittest.TestCase.__init__(self, *args, **kwargs)
62
def parse_xml(self, xml_string):
64
xml = ET.fromstring(xml_string)
66
except (SyntaxError, ExpatError) as e:
67
msg = e.msg if hasattr(e, 'msg') else e.message
70
'stream': 'http://etherx.jabber.org/streams'}
72
prefix = xml_string.split('<')[1].split(':')[0]
73
if prefix in known_prefixes:
74
xml_string = '<fixns xmlns:%s="%s">%s</fixns>' % (
76
known_prefixes[prefix],
78
xml = self.parse_xml(xml_string)
82
self.fail("XML data was mal-formed:\n%s" % xml_string)
84
# ------------------------------------------------------------------
85
# Shortcut methods for creating stanza objects
87
def Message(self, *args, **kwargs):
89
Create a Message stanza.
91
Uses same arguments as StanzaBase.__init__
94
xml -- An XML object to use for the Message's values.
96
return Message(self.xmpp, *args, **kwargs)
98
def Iq(self, *args, **kwargs):
102
Uses same arguments as StanzaBase.__init__
105
xml -- An XML object to use for the Iq's values.
107
return Iq(self.xmpp, *args, **kwargs)
109
def Presence(self, *args, **kwargs):
111
Create a Presence stanza.
113
Uses same arguments as StanzaBase.__init__
116
xml -- An XML object to use for the Iq's values.
118
return Presence(self.xmpp, *args, **kwargs)
120
def check_jid(self, jid, user=None, domain=None, resource=None,
121
bare=None, full=None, string=None):
123
Verify the components of a JID.
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.
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)
144
self.assertEqual(jid.bare, bare,
145
"Bare JID does not match: %s" % jid.bare)
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))
153
def check_roster(self, owner, jid, name=None, subscription=None,
154
afrom=None, ato=None, pending_out=None, pending_in=None,
156
roster = self.xmpp.roster[owner][jid]
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'])
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'])
179
# ------------------------------------------------------------------
180
# Methods for comparing stanza objects to XML strings
182
def check(self, stanza, criteria, method='exact',
183
defaults=None, use_values=True):
185
Create and compare several stanza objects to a correct XML string.
187
If use_values is False, tests using stanza.values will not be used.
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.
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.
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.
213
if method is None and hasattr(self, 'match_method'):
214
method = getattr(self, 'match_method')
216
if method != 'exact':
217
matchers = {'stanzapath': StanzaPath,
219
'mask': MatchXMLMask,
221
Matcher = matchers.get(method, 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))
230
stanza_class = stanza.__class__
231
if not isinstance(criteria, ElementBase):
232
xml = self.parse_xml(criteria)
236
# Ensure that top level namespaces are used, even if they
238
self.fix_namespaces(stanza.xml, 'jabber:client')
239
self.fix_namespaces(xml, 'jabber:client')
241
stanza2 = stanza_class(xml=xml)
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
248
default_stanza = stanza_class()
252
Presence: ['priority']
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
266
values = stanza2.values
267
stanza3 = stanza_class()
268
stanza3.values = values
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)
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)
283
self.failUnless(result, debug)
285
# ------------------------------------------------------------------
286
# Methods for simulating stanza streams.
288
def stream_disconnect(self):
290
Simulate a stream disconnection.
293
self.xmpp.socket.disconnect_error()
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={}):
301
Initialize an XMPP client or component using a dummy XML stream.
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.
316
server -- The name of the XMPP server. Defaults to 'localhost'.
317
port -- The port to use when connecting to the server.
319
plugins -- List of plugins to register. By default, all plugins
323
self.xmpp = ClientXMPP(jid, password,
325
plugin_config=plugin_config)
326
elif mode == 'component':
327
self.xmpp = ComponentXMPP(jid, password,
329
plugin_config=plugin_config)
331
raise ValueError("Unknown XMPP connection mode.")
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
339
# We will use this to wait for the session_start event
340
# for live connections.
341
skip_queue = queue.Queue()
344
self.xmpp.set_socket(TestSocket())
346
# Simulate connecting for mock sockets.
347
self.xmpp.auto_reconnect = False
348
self.xmpp.state._set_state('connected')
350
# Must have the stream header ready for xmpp.process() to work.
352
header = self.xmpp.stream_header
353
self.xmpp.socket.recv_data(header)
354
elif socket == 'live':
355
self.xmpp.socket_class = TestLiveSocket
357
def wait_for_session(x):
358
self.xmpp.socket.clear()
359
skip_queue.put('started')
361
self.xmpp.add_event_handler('session_start', wait_for_session)
362
if server is not None:
363
self.xmpp.connect((server, port))
367
raise ValueError("Unknown socket type.")
370
self.xmpp.register_plugins()
372
for plugin in plugins:
373
self.xmpp.register_plugin(plugin)
374
self.xmpp.process(threaded=True)
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)
384
skip_queue.get(block=True, timeout=10)
386
def make_header(self, sto='',
389
stream_ns="http://etherx.jabber.org/streams",
390
default_ns="jabber:client",
395
Create a stream header to be received by the test XMPP agent.
397
The header must be saved and passed to stream_start.
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.
409
header = '<stream:stream %s>'
412
header = '<?xml version="1.0"?>' + header
414
parts.append('to="%s"' % sto)
416
parts.append('from="%s"' % sfrom)
418
parts.append('id="%s"' % sid)
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)
426
def recv(self, data, defaults=[], method='exact',
427
use_values=True, timeout=1):
429
Pass data to the dummy XMPP client as if it came from an XMPP server.
431
If using a live connection, verify what the server has sent.
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
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
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,
461
use_values=use_values)
463
# place the data in the dummy socket receiving queue.
465
self.xmpp.socket.recv_data(data)
467
def recv_header(self, sto='',
470
stream_ns="http://etherx.jabber.org/streams",
471
default_ns="jabber:client",
476
Check that a given stream header was received.
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
490
header = self.make_header(sto, sfrom, sid,
492
default_ns=default_ns,
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.")
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>'
504
xml = self.parse_xml(header2)
505
recv_xml = self.parse_xml(recv_header2)
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']
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]
521
# We received more than just the header
523
self.xmpp.socket.recv_data(tostring(xml))
525
attrib = recv_xml.attrib
527
recv_xml.attrib = attrib
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)))
535
def recv_feature(self, data, method='mask', use_values=True, timeout=1):
538
if method is None and hasattr(self, 'match_method'):
539
method = getattr(self, 'match_method')
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
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))
561
raise ValueError("Uknown matching method: %s" % method)
563
# place the data in the dummy socket receiving queue.
565
self.xmpp.socket.recv_data(data)
567
def send_header(self, sto='',
570
stream_ns="http://etherx.jabber.org/streams",
571
default_ns="jabber:client",
577
Check that a given stream header was sent.
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
591
header = self.make_header(sto, sfrom, sid,
593
default_ns=default_ns,
594
default_lang=default_lang,
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.")
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>'
606
xml = self.parse_xml(header2)
607
sent_xml = self.parse_xml(sent_header2)
610
self.compare(xml, sent_xml),
611
"Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % (
612
header, sent_header))
614
def send_feature(self, data, method='mask', use_values=True, timeout=1):
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))
633
raise ValueError("Uknown matching method: %s" % method)
635
def send(self, data, defaults=None, use_values=True,
636
timeout=.5, method='exact'):
638
Check that the XMPP client sent the given stanza XML.
640
Extracts the next sent stanza and compares it with the given
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
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.
657
sent = self.xmpp.socket.next_sent(timeout)
658
if data is None and sent is None:
660
if data is None and sent is not None:
661
self.fail("Stanza data was sent: %s" % sent)
663
self.fail("No stanza was sent.")
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,
671
use_values=use_values)
673
def stream_close(self):
675
Disconnect the dummy XMPP client.
677
Can be safely called even if stream_start has not been called.
679
Must be placed in the tearDown method of a test class to ensure
680
that the XMPP client is disconnected after an error.
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()
686
# ------------------------------------------------------------------
687
# XML Comparison and Cleanup
689
def fix_namespaces(self, xml, ns):
691
Assign a namespace to an element and any children that
692
don't have a namespace.
695
xml -- The XML object to fix.
696
ns -- The namespace to add to the XML object.
698
if xml.tag.startswith('{'):
700
xml.tag = '{%s}%s' % (ns, xml.tag)
702
self.fix_namespaces(child, ns)
704
def compare(self, xml, *other):
709
xml -- The XML object to compare against.
710
*other -- The list of XML objects to compare.
715
# Compare multiple objects
718
if not self.compare(xml, xml2):
725
if xml.tag != other.tag:
728
# Step 2: Check attributes
729
if xml.attrib != other.attrib:
735
if other.text is None:
737
xml.text = xml.text.strip()
738
other.text = other.text.strip()
740
if xml.text != other.text:
743
# Step 4: Check children count
744
if len(list(xml)) != len(list(other)):
747
# Step 5: Recursively check children
749
child2s = other.findall("%s" % child.tag)
752
for child2 in child2s:
753
if self.compare(child, child2):
758
# Step 6: Recursively check children the other way.
760
child2s = xml.findall("%s" % child.tag)
763
for child2 in child2s:
764
if self.compare(child, child2):