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
12 from sleekxmpp
import ClientXMPP
, ComponentXMPP
13 from sleekxmpp
.util
import Queue
14 from sleekxmpp
.stanza
import Message
, Iq
, Presence
15 from sleekxmpp
.test
import TestSocket
, TestLiveSocket
16 from sleekxmpp
.xmlstream
import ET
17 from sleekxmpp
.xmlstream
import ElementBase
18 from sleekxmpp
.xmlstream
.tostring
import tostring
19 from sleekxmpp
.xmlstream
.matcher
import StanzaPath
, MatcherId
, MatchIDSender
20 from sleekxmpp
.xmlstream
.matcher
import MatchXMLMask
, MatchXPath
23 class SleekTest(unittest
.TestCase
):
26 A SleekXMPP specific TestCase class that provides
27 methods for comparing message, iq, and presence stanzas.
30 Message -- Create a Message stanza object.
31 Iq -- Create an Iq stanza object.
32 Presence -- Create a Presence stanza object.
33 check_jid -- Check a JID and its component parts.
34 check -- Compare a stanza against an XML string.
35 stream_start -- Initialize a dummy XMPP client.
36 stream_close -- Disconnect the XMPP client.
37 make_header -- Create a stream header.
38 send_header -- Check that the given header has been sent.
39 send_feature -- Send a raw XML element.
40 send -- Check that the XMPP client sent the given
42 recv -- Queue data for XMPP client to receive, or
43 verify the data that was received from a
45 recv_header -- Check that a given stream header
47 recv_feature -- Check that a given, raw XML element
49 fix_namespaces -- Add top-level namespace to an XML object.
50 compare -- Compare XML objects against each other.
53 def __init__(self
, *args
, **kwargs
):
54 unittest
.TestCase
.__init
__(self
, *args
, **kwargs
)
57 def parse_xml(self
, xml_string
):
59 xml
= ET
.fromstring(xml_string
)
61 except (SyntaxError, ExpatError
) as e
:
62 msg
= e
.msg
if hasattr(e
, 'msg') else e
.message
65 'stream': 'http://etherx.jabber.org/streams'}
67 prefix
= xml_string
.split('<')[1].split(':')[0]
68 if prefix
in known_prefixes
:
69 xml_string
= '<fixns xmlns:%s="%s">%s</fixns>' % (
71 known_prefixes
[prefix
],
73 xml
= self
.parse_xml(xml_string
)
77 self
.fail("XML data was mal-formed:\n%s" % xml_string
)
79 # ------------------------------------------------------------------
80 # Shortcut methods for creating stanza objects
82 def Message(self
, *args
, **kwargs
):
84 Create a Message stanza.
86 Uses same arguments as StanzaBase.__init__
89 xml -- An XML object to use for the Message's values.
91 return Message(self
.xmpp
, *args
, **kwargs
)
93 def Iq(self
, *args
, **kwargs
):
97 Uses same arguments as StanzaBase.__init__
100 xml -- An XML object to use for the Iq's values.
102 return Iq(self
.xmpp
, *args
, **kwargs
)
104 def Presence(self
, *args
, **kwargs
):
106 Create a Presence stanza.
108 Uses same arguments as StanzaBase.__init__
111 xml -- An XML object to use for the Iq's values.
113 return Presence(self
.xmpp
, *args
, **kwargs
)
115 def check_jid(self
, jid
, user
=None, domain
=None, resource
=None,
116 bare
=None, full
=None, string
=None):
118 Verify the components of a JID.
121 jid -- The JID object to test.
122 user -- Optional. The user name portion of the JID.
123 domain -- Optional. The domain name portion of the JID.
124 resource -- Optional. The resource portion of the JID.
125 bare -- Optional. The bare JID.
126 full -- Optional. The full JID.
127 string -- Optional. The string version of the JID.
130 self
.assertEqual(jid
.user
, user
,
131 "User does not match: %s" % jid
.user
)
132 if domain
is not None:
133 self
.assertEqual(jid
.domain
, domain
,
134 "Domain does not match: %s" % jid
.domain
)
135 if resource
is not None:
136 self
.assertEqual(jid
.resource
, resource
,
137 "Resource does not match: %s" % jid
.resource
)
139 self
.assertEqual(jid
.bare
, bare
,
140 "Bare JID does not match: %s" % jid
.bare
)
142 self
.assertEqual(jid
.full
, full
,
143 "Full JID does not match: %s" % jid
.full
)
144 if string
is not None:
145 self
.assertEqual(str(jid
), string
,
146 "String does not match: %s" % str(jid
))
148 def check_roster(self
, owner
, jid
, name
=None, subscription
=None,
149 afrom
=None, ato
=None, pending_out
=None, pending_in
=None,
151 roster
= self
.xmpp
.roster
[owner
][jid
]
153 self
.assertEqual(roster
['name'], name
,
154 "Incorrect name value: %s" % roster
['name'])
155 if subscription
is not None:
156 self
.assertEqual(roster
['subscription'], subscription
,
157 "Incorrect subscription: %s" % roster
['subscription'])
158 if afrom
is not None:
159 self
.assertEqual(roster
['from'], afrom
,
160 "Incorrect from state: %s" % roster
['from'])
162 self
.assertEqual(roster
['to'], ato
,
163 "Incorrect to state: %s" % roster
['to'])
164 if pending_out
is not None:
165 self
.assertEqual(roster
['pending_out'], pending_out
,
166 "Incorrect pending_out state: %s" % roster
['pending_out'])
167 if pending_in
is not None:
168 self
.assertEqual(roster
['pending_in'], pending_out
,
169 "Incorrect pending_in state: %s" % roster
['pending_in'])
170 if groups
is not None:
171 self
.assertEqual(roster
['groups'], groups
,
172 "Incorrect groups: %s" % roster
['groups'])
174 # ------------------------------------------------------------------
175 # Methods for comparing stanza objects to XML strings
177 def check(self
, stanza
, criteria
, method
='exact',
178 defaults
=None, use_values
=True):
180 Create and compare several stanza objects to a correct XML string.
182 If use_values is False, tests using stanza.values will not be used.
184 Some stanzas provide default values for some interfaces, but
185 these defaults can be problematic for testing since they can easily
186 be forgotten when supplying the XML string. A list of interfaces that
187 use defaults may be provided and the generated stanzas will use the
188 default values for those interfaces if needed.
190 However, correcting the supplied XML is not possible for interfaces
191 that add or remove XML elements. Only interfaces that map to XML
192 attributes may be set using the defaults parameter. The supplied XML
193 must take into account any extra elements that are included by default.
196 stanza -- The stanza object to test.
197 criteria -- An expression the stanza must match against.
198 method -- The type of matching to use; one of:
199 'exact', 'mask', 'id', 'xpath', and 'stanzapath'.
200 Defaults to the value of self.match_method.
201 defaults -- A list of stanza interfaces that have default
202 values. These interfaces will be set to their
203 defaults for the given and generated stanzas to
204 prevent unexpected test failures.
205 use_values -- Indicates if testing using stanza.values should
206 be used. Defaults to True.
208 if method
is None and hasattr(self
, 'match_method'):
209 method
= getattr(self
, 'match_method')
211 if method
!= 'exact':
212 matchers
= {'stanzapath': StanzaPath
,
214 'mask': MatchXMLMask
,
215 'idsender': MatchIDSender
,
217 Matcher
= matchers
.get(method
, None)
219 raise ValueError("Unknown matching method.")
220 test
= Matcher(criteria
)
221 self
.failUnless(test
.match(stanza
),
222 "Stanza did not match using %s method:\n" % method
+ \
223 "Criteria:\n%s\n" % str(criteria
) + \
224 "Stanza:\n%s" % str(stanza
))
226 stanza_class
= stanza
.__class
__
227 if not isinstance(criteria
, ElementBase
):
228 xml
= self
.parse_xml(criteria
)
232 # Ensure that top level namespaces are used, even if they
234 self
.fix_namespaces(stanza
.xml
, 'jabber:client')
235 self
.fix_namespaces(xml
, 'jabber:client')
237 stanza2
= stanza_class(xml
=xml
)
240 # Using stanza.values will add XML for any interface that
241 # has a default value. We need to set those defaults on
242 # the existing stanzas and XML so that they will compare
244 default_stanza
= stanza_class()
248 Presence
: ['priority']
250 defaults
= known_defaults
.get(stanza_class
, [])
251 for interface
in defaults
:
252 stanza
[interface
] = stanza
[interface
]
253 stanza2
[interface
] = stanza2
[interface
]
254 # Can really only automatically add defaults for top
255 # level attribute values. Anything else must be accounted
256 # for in the provided XML string.
257 if interface
not in xml
.attrib
:
258 if interface
in default_stanza
.xml
.attrib
:
259 value
= default_stanza
.xml
.attrib
[interface
]
260 xml
.attrib
[interface
] = value
262 values
= stanza2
.values
263 stanza3
= stanza_class()
264 stanza3
.values
= values
266 debug
= "Three methods for creating stanzas do not match.\n"
267 debug
+= "Given XML:\n%s\n" % tostring(xml
)
268 debug
+= "Given stanza:\n%s\n" % tostring(stanza
.xml
)
269 debug
+= "Generated stanza:\n%s\n" % tostring(stanza2
.xml
)
270 debug
+= "Second generated stanza:\n%s\n" % tostring(stanza3
.xml
)
271 result
= self
.compare(xml
, stanza
.xml
, stanza2
.xml
, stanza3
.xml
)
273 debug
= "Two methods for creating stanzas do not match.\n"
274 debug
+= "Given XML:\n%s\n" % tostring(xml
)
275 debug
+= "Given stanza:\n%s\n" % tostring(stanza
.xml
)
276 debug
+= "Generated stanza:\n%s\n" % tostring(stanza2
.xml
)
277 result
= self
.compare(xml
, stanza
.xml
, stanza2
.xml
)
279 self
.failUnless(result
, debug
)
281 # ------------------------------------------------------------------
282 # Methods for simulating stanza streams.
284 def stream_disconnect(self
):
286 Simulate a stream disconnection.
289 self
.xmpp
.socket
.disconnect_error()
291 def stream_start(self
, mode
='client', skip
=True, header
=None,
292 socket
='mock', jid
='tester@localhost',
293 password
='test', server
='localhost',
294 port
=5222, sasl_mech
=None,
295 plugins
=None, plugin_config
={}):
297 Initialize an XMPP client or component using a dummy XML stream.
300 mode -- Either 'client' or 'component'. Defaults to 'client'.
301 skip -- Indicates if the first item in the sent queue (the
302 stream header) should be removed. Tests that wish
303 to test initializing the stream should set this to
304 False. Otherwise, the default of True should be used.
305 socket -- Either 'mock' or 'live' to indicate if the socket
306 should be a dummy, mock socket or a live, functioning
307 socket. Defaults to 'mock'.
308 jid -- The JID to use for the connection.
309 Defaults to 'tester@localhost'.
310 password -- The password to use for the connection.
312 server -- The name of the XMPP server. Defaults to 'localhost'.
313 port -- The port to use when connecting to the server.
315 plugins -- List of plugins to register. By default, all plugins
319 self
.xmpp
= ClientXMPP(jid
, password
,
321 plugin_config
=plugin_config
)
322 elif mode
== 'component':
323 self
.xmpp
= ComponentXMPP(jid
, password
,
325 plugin_config
=plugin_config
)
327 raise ValueError("Unknown XMPP connection mode.")
329 # Remove unique ID prefix to make it easier to test
330 self
.xmpp
._id
_prefix
= ''
331 self
.xmpp
._disconnect
_wait
_for
_threads
= False
332 self
.xmpp
.default_lang
= None
333 self
.xmpp
.peer_default_lang
= None
335 # We will use this to wait for the session_start event
336 # for live connections.
340 self
.xmpp
.set_socket(TestSocket())
342 # Simulate connecting for mock sockets.
343 self
.xmpp
.auto_reconnect
= False
344 self
.xmpp
.state
._set
_state
('connected')
346 # Must have the stream header ready for xmpp.process() to work.
348 header
= self
.xmpp
.stream_header
349 self
.xmpp
.socket
.recv_data(header
)
350 elif socket
== 'live':
351 self
.xmpp
.socket_class
= TestLiveSocket
353 def wait_for_session(x
):
354 self
.xmpp
.socket
.clear()
355 skip_queue
.put('started')
357 self
.xmpp
.add_event_handler('session_start', wait_for_session
)
358 if server
is not None:
359 self
.xmpp
.connect((server
, port
))
363 raise ValueError("Unknown socket type.")
366 self
.xmpp
.register_plugins()
368 for plugin
in plugins
:
369 self
.xmpp
.register_plugin(plugin
)
371 # Some plugins require messages to have ID values. Set
372 # this to True in tests related to those plugins.
373 self
.xmpp
.use_message_ids
= False
375 self
.xmpp
.process(threaded
=True)
378 # Mark send queue as usable
379 self
.xmpp
.session_bind_event
.set()
380 self
.xmpp
.session_started_event
.set()
381 # Clear startup stanzas
382 self
.xmpp
.socket
.next_sent(timeout
=1)
383 if mode
== 'component':
384 self
.xmpp
.socket
.next_sent(timeout
=1)
386 skip_queue
.get(block
=True, timeout
=10)
388 def make_header(self
, sto
='',
391 stream_ns
="http://etherx.jabber.org/streams",
392 default_ns
="jabber:client",
397 Create a stream header to be received by the test XMPP agent.
399 The header must be saved and passed to stream_start.
402 sto -- The recipient of the stream header.
403 sfrom -- The agent sending the stream header.
404 sid -- The stream's id.
405 stream_ns -- The namespace of the stream's root element.
406 default_ns -- The default stanza namespace.
407 version -- The stream version.
408 xml_header -- Indicates if the XML version header should be
409 appended before the stream header.
411 header
= '<stream:stream %s>'
414 header
= '<?xml version="1.0"?>' + header
416 parts
.append('to="%s"' % sto
)
418 parts
.append('from="%s"' % sfrom
)
420 parts
.append('id="%s"' % sid
)
422 parts
.append('xml:lang="%s"' % default_lang
)
423 parts
.append('version="%s"' % version
)
424 parts
.append('xmlns:stream="%s"' % stream_ns
)
425 parts
.append('xmlns="%s"' % default_ns
)
426 return header
% ' '.join(parts
)
428 def recv(self
, data
, defaults
=[], method
='exact',
429 use_values
=True, timeout
=1):
431 Pass data to the dummy XMPP client as if it came from an XMPP server.
433 If using a live connection, verify what the server has sent.
436 data -- If a dummy socket is being used, the XML that is to
437 be received next. Otherwise it is the criteria used
438 to match against live data that is received.
439 defaults -- A list of stanza interfaces with default values that
440 may interfere with comparisons.
441 method -- Select the type of comparison to use for
442 verifying the received stanza. Options are 'exact',
443 'id', 'stanzapath', 'xpath', and 'mask'.
444 Defaults to the value of self.match_method.
445 use_values -- Indicates if stanza comparisons should test using
446 stanza.values. Defaults to True.
447 timeout -- Time to wait in seconds for data to be received by
450 if self
.xmpp
.socket
.is_live
:
451 # we are working with a live connection, so we should
452 # verify what has been received instead of simulating
454 recv_data
= self
.xmpp
.socket
.next_recv(timeout
)
455 if recv_data
is None:
456 self
.fail("No stanza was received.")
457 xml
= self
.parse_xml(recv_data
)
458 self
.fix_namespaces(xml
, 'jabber:client')
459 stanza
= self
.xmpp
._build
_stanza
(xml
, 'jabber:client')
460 self
.check(stanza
, data
,
463 use_values
=use_values
)
465 # place the data in the dummy socket receiving queue.
467 self
.xmpp
.socket
.recv_data(data
)
469 def recv_header(self
, sto
='',
472 stream_ns
="http://etherx.jabber.org/streams",
473 default_ns
="jabber:client",
478 Check that a given stream header was received.
481 sto -- The recipient of the stream header.
482 sfrom -- The agent sending the stream header.
483 sid -- The stream's id. Set to None to ignore.
484 stream_ns -- The namespace of the stream's root element.
485 default_ns -- The default stanza namespace.
486 version -- The stream version.
487 xml_header -- Indicates if the XML version header should be
488 appended before the stream header.
489 timeout -- Length of time to wait in seconds for a
492 header
= self
.make_header(sto
, sfrom
, sid
,
494 default_ns
=default_ns
,
496 xml_header
=xml_header
)
497 recv_header
= self
.xmpp
.socket
.next_recv(timeout
)
498 if recv_header
is None:
499 raise ValueError("Socket did not return data.")
501 # Apply closing elements so that we can construct
502 # XML objects for comparison.
503 header2
= header
+ '</stream:stream>'
504 recv_header2
= recv_header
+ '</stream:stream>'
506 xml
= self
.parse_xml(header2
)
507 recv_xml
= self
.parse_xml(recv_header2
)
510 # Ignore the id sent by the server since
511 # we can't know in advance what it will be.
512 if 'id' in recv_xml
.attrib
:
513 del recv_xml
.attrib
['id']
515 # Ignore the xml:lang attribute for now.
516 if 'xml:lang' in recv_xml
.attrib
:
517 del recv_xml
.attrib
['xml:lang']
518 xml_ns
= 'http://www.w3.org/XML/1998/namespace'
519 if '{%s}lang' % xml_ns
in recv_xml
.attrib
:
520 del recv_xml
.attrib
['{%s}lang' % xml_ns
]
523 # We received more than just the header
525 self
.xmpp
.socket
.recv_data(tostring(xml
))
527 attrib
= recv_xml
.attrib
529 recv_xml
.attrib
= attrib
532 self
.compare(xml
, recv_xml
),
533 "Stream headers do not match:\nDesired:\n%s\nReceived:\n%s" % (
534 '%s %s' % (xml
.tag
, xml
.attrib
),
535 '%s %s' % (recv_xml
.tag
, recv_xml
.attrib
)))
537 def recv_feature(self
, data
, method
='mask', use_values
=True, timeout
=1):
540 if method
is None and hasattr(self
, 'match_method'):
541 method
= getattr(self
, 'match_method')
543 if self
.xmpp
.socket
.is_live
:
544 # we are working with a live connection, so we should
545 # verify what has been received instead of simulating
547 recv_data
= self
.xmpp
.socket
.next_recv(timeout
)
548 xml
= self
.parse_xml(data
)
549 recv_xml
= self
.parse_xml(recv_data
)
550 if recv_data
is None:
551 self
.fail("No stanza was received.")
552 if method
== 'exact':
553 self
.failUnless(self
.compare(xml
, recv_xml
),
554 "Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
555 tostring(xml
), tostring(recv_xml
)))
556 elif method
== 'mask':
557 matcher
= MatchXMLMask(xml
)
558 self
.failUnless(matcher
.match(recv_xml
),
559 "Stanza did not match using %s method:\n" % method
+ \
560 "Criteria:\n%s\n" % tostring(xml
) + \
561 "Stanza:\n%s" % tostring(recv_xml
))
563 raise ValueError("Uknown matching method: %s" % method
)
565 # place the data in the dummy socket receiving queue.
567 self
.xmpp
.socket
.recv_data(data
)
569 def send_header(self
, sto
='',
572 stream_ns
="http://etherx.jabber.org/streams",
573 default_ns
="jabber:client",
579 Check that a given stream header was sent.
582 sto -- The recipient of the stream header.
583 sfrom -- The agent sending the stream header.
584 sid -- The stream's id.
585 stream_ns -- The namespace of the stream's root element.
586 default_ns -- The default stanza namespace.
587 version -- The stream version.
588 xml_header -- Indicates if the XML version header should be
589 appended before the stream header.
590 timeout -- Length of time to wait in seconds for a
593 header
= self
.make_header(sto
, sfrom
, sid
,
595 default_ns
=default_ns
,
596 default_lang
=default_lang
,
598 xml_header
=xml_header
)
599 sent_header
= self
.xmpp
.socket
.next_sent(timeout
)
600 if sent_header
is None:
601 raise ValueError("Socket did not return data.")
603 # Apply closing elements so that we can construct
604 # XML objects for comparison.
605 header2
= header
+ '</stream:stream>'
606 sent_header2
= sent_header
+ b
'</stream:stream>'
608 xml
= self
.parse_xml(header2
)
609 sent_xml
= self
.parse_xml(sent_header2
)
612 self
.compare(xml
, sent_xml
),
613 "Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % (
614 header
, sent_header
))
616 def send_feature(self
, data
, method
='mask', use_values
=True, timeout
=1):
619 sent_data
= self
.xmpp
.socket
.next_sent(timeout
)
620 xml
= self
.parse_xml(data
)
621 sent_xml
= self
.parse_xml(sent_data
)
622 if sent_data
is None:
623 self
.fail("No stanza was sent.")
624 if method
== 'exact':
625 self
.failUnless(self
.compare(xml
, sent_xml
),
626 "Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
627 tostring(xml
), tostring(sent_xml
)))
628 elif method
== 'mask':
629 matcher
= MatchXMLMask(xml
)
630 self
.failUnless(matcher
.match(sent_xml
),
631 "Stanza did not match using %s method:\n" % method
+ \
632 "Criteria:\n%s\n" % tostring(xml
) + \
633 "Stanza:\n%s" % tostring(sent_xml
))
635 raise ValueError("Uknown matching method: %s" % method
)
637 def send(self
, data
, defaults
=None, use_values
=True,
638 timeout
=.5, method
='exact'):
640 Check that the XMPP client sent the given stanza XML.
642 Extracts the next sent stanza and compares it with the given
646 stanza_class -- The class of the sent stanza object.
647 data -- The XML string of the expected Message stanza,
648 or an equivalent stanza object.
649 use_values -- Modifies the type of tests used by check_message.
650 defaults -- A list of stanza interfaces that have defaults
651 values which may interfere with comparisons.
652 timeout -- Time in seconds to wait for a stanza before
654 method -- Select the type of comparison to use for
655 verifying the sent stanza. Options are 'exact',
656 'id', 'stanzapath', 'xpath', and 'mask'.
657 Defaults to the value of self.match_method.
659 sent
= self
.xmpp
.socket
.next_sent(timeout
)
660 if data
is None and sent
is None:
662 if data
is None and sent
is not None:
663 self
.fail("Stanza data was sent: %s" % sent
)
665 self
.fail("No stanza was sent.")
667 xml
= self
.parse_xml(sent
)
668 self
.fix_namespaces(xml
, 'jabber:client')
669 sent
= self
.xmpp
._build
_stanza
(xml
, 'jabber:client')
670 self
.check(sent
, data
,
673 use_values
=use_values
)
675 def stream_close(self
):
677 Disconnect the dummy XMPP client.
679 Can be safely called even if stream_start has not been called.
681 Must be placed in the tearDown method of a test class to ensure
682 that the XMPP client is disconnected after an error.
684 if hasattr(self
, 'xmpp') and self
.xmpp
is not None:
685 self
.xmpp
.socket
.recv_data(self
.xmpp
.stream_footer
)
686 self
.xmpp
.disconnect()
688 # ------------------------------------------------------------------
689 # XML Comparison and Cleanup
691 def fix_namespaces(self
, xml
, ns
):
693 Assign a namespace to an element and any children that
694 don't have a namespace.
697 xml -- The XML object to fix.
698 ns -- The namespace to add to the XML object.
700 if xml
.tag
.startswith('{'):
702 xml
.tag
= '{%s}%s' % (ns
, xml
.tag
)
704 self
.fix_namespaces(child
, ns
)
706 def compare(self
, xml
, *other
):
711 xml -- The XML object to compare against.
712 *other -- The list of XML objects to compare.
717 # Compare multiple objects
720 if not self
.compare(xml
, xml2
):
727 if xml
.tag
!= other
.tag
:
730 # Step 2: Check attributes
731 if xml
.attrib
!= other
.attrib
:
737 if other
.text
is None:
739 xml
.text
= xml
.text
.strip()
740 other
.text
= other
.text
.strip()
742 if xml
.text
!= other
.text
:
745 # Step 4: Check children count
746 if len(list(xml
)) != len(list(other
)):
749 # Step 5: Recursively check children
751 child2s
= other
.findall("%s" % child
.tag
)
754 for child2
in child2s
:
755 if self
.compare(child
, child2
):
760 # Step 6: Recursively check children the other way.
762 child2s
= xml
.findall("%s" % child
.tag
)
765 for child2
in child2s
:
766 if self
.compare(child
, child2
):