Compare commits

...

15 Commits

Author SHA1 Message Date
Lance Stout
5bdcd9ef9d Made exceptions work.
Raising an XMPPError exception from an event handler now works, even if
from a threaded handler.

Added stream tests to verify.

We should start using XMPPError, it really makes things simple!
2010-10-25 15:09:56 -04:00
Lance Stout
2eff35cc7a Added more presence stream tests.
Tests auto_authorize=False, and got_online.
2010-10-25 13:21:00 -04:00
Lance Stout
ac330b5c6c Fixed bug in presence subscription handling.
Subscription requests and responses were not setting the correct 'to'
attribute.
2010-10-25 12:52:32 -04:00
Lance Stout
46ffa8e9fe Added stream tests for presence events.
First batch of tests, currently focuses on the got_offline event.
2010-10-24 19:57:07 -04:00
Lance Stout
03847497cc Added test for error stanzas. 2010-10-24 19:56:42 -04:00
Lance Stout
185d7cf28e More JID unit tests.
sleekxmpp.xmlstream.jid now has 100% coverage!
2010-10-24 19:06:54 -04:00
Lance Stout
8aa3d0c047 Fixed got_offline triggering bug. 2010-10-24 18:56:50 -04:00
Lance Stout
9e3d506651 Fixed resource bug in JIDs.
JIDs without resources will return '' instead of the bare JID.

Cleaned up JID tests, and added check_JID to SleekTest.
2010-10-24 18:22:41 -04:00
Lance Stout
2f3ff37a24 Make SleekTest streams register all plugins.
Makes test coverage nicer.
2010-10-24 17:35:11 -04:00
Lance Stout
1f09d60a52 ComponentXMPP saves all of its config data now.
ComponentXMPP was ignoring plugin_config and plugin_whitelist
parameters, making register_plugins() fail.
2010-10-24 17:33:11 -04:00
Lance Stout
d528884723 Added stream tests for rosters. 2010-10-24 12:53:14 -04:00
Lance Stout
d9aff3d36f Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop 2010-10-24 12:11:34 -04:00
Lance Stout
04cc48775d Fixed error in client roster handling.
The roster result iq was not being passed to the roster update
handler.
2010-10-24 12:08:59 -04:00
Nathan Fritz
27ebb6e8f6 presence no longer replies when exception is caught and tweaks to presence events 2010-10-21 16:59:15 -07:00
Lance Stout
8f55704928 Fixed mixed text and elements bug in tostring.
XML of the form <a>foo <b>bar</b> baz</a> was outputted as
<a>foo <b>bar</b> baz baz</a>.

Includes unit test.
2010-10-21 16:21:28 -04:00
15 changed files with 612 additions and 39 deletions

View File

@@ -553,6 +553,7 @@ class BaseXMPP(XMLStream):
priority = presence['priority']
was_offline = False
got_online = False
old_roster = self.roster.get(jid, {}).get(resource, {})
# Create a new roster entry if needed.
@@ -569,7 +570,7 @@ class BaseXMPP(XMLStream):
# Determine if the user has just come online.
if not resource in connections:
if show == 'available' or show in presence.showtypes:
self.event("got_online", presence)
got_online = True
was_offline = True
connections[resource] = {}
@@ -601,6 +602,8 @@ class BaseXMPP(XMLStream):
# Presence state has changed.
self.event("changed_status", presence)
if got_online:
self.event("got_online", presence)
logging.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource,
show, status))
@@ -618,8 +621,8 @@ class BaseXMPP(XMLStream):
None * Disable automatic handling and use
a custom handler.
"""
presence = self.Presence()
presence['to'] = presence['from'].bare
presence.reply()
presence['to'] = presence['to'].bare
# We are using trinary logic, so conditions have to be
# more explicit than usual.

View File

@@ -219,8 +219,8 @@ class ClientXMPP(BaseXMPP):
iq['roster']['items'] = {jid: {'name': name,
'subscription': subscription,
'groups': groups}}
resp = iq.send()
return resp['type'] == 'result'
response = iq.send()
return response['type'] == 'result'
def del_roster_item(self, jid):
"""
@@ -235,8 +235,8 @@ class ClientXMPP(BaseXMPP):
def get_roster(self):
"""Request the roster from the server."""
iq = self.Iq()._set_stanza_values({'type': 'get'}).enable('roster')
iq.send()
self._handle_roster(iq, request=True)
response = iq.send()
self._handle_roster(response, request=True)
def _handle_stream_features(self, features):
"""

View File

@@ -67,6 +67,8 @@ class ComponentXMPP(BaseXMPP):
self.server_port = port
self.set_jid(jid)
self.secret = secret
self.plugin_config = plugin_config
self.plugin_whitelist = plugin_whitelist
self.is_component = True
self.register_handler(

View File

@@ -38,6 +38,9 @@ class XMPPError(Exception):
element. Same as the additional arguments to
the ET.Element constructor.
"""
if extension_args is None:
extension_args = {}
self.condition = condition
self.text = text
self.etype = etype

View File

@@ -92,6 +92,12 @@ class Presence(RootStanza):
return StanzaBase.setup(self, xml)
def exception(self, e):
"""
Override exception passback for presence.
"""
pass
def set_show(self, show):
"""
Set the value of the <show> element.

View File

@@ -103,6 +103,41 @@ class SleekTest(unittest.TestCase):
"""
return Presence(None, *args, **kwargs)
def check_JID(self, jid, user=None, domain=None, resource=None,
bare=None, full=None, string=None):
"""
Verify the components of a JID.
Arguments:
jid -- The JID object to test.
user -- Optional. The user name portion of the JID.
domain -- Optional. The domain name portion of the JID.
resource -- Optional. The resource portion of the JID.
bare -- Optional. The bare JID.
full -- Optional. The full JID.
string -- Optional. The string version of the JID.
"""
if user is not None:
self.assertEqual(jid.user, user,
"User does not match: %s" % jid.user)
if domain is not None:
self.assertEqual(jid.domain, domain,
"Domain does not match: %s" % jid.domain)
if resource is not None:
self.assertEqual(jid.resource, resource,
"Resource does not match: %s" % jid.resource)
if bare is not None:
self.assertEqual(jid.bare, bare,
"Bare JID does not match: %s" % jid.bare)
if full is not None:
self.assertEqual(jid.full, full,
"Full JID does not match: %s" % jid.full)
if string is not None:
self.assertEqual(str(jid), string,
"String does not match: %s" % str(jid))
# ------------------------------------------------------------------
# Methods for comparing stanza objects to XML strings
@@ -294,6 +329,7 @@ class SleekTest(unittest.TestCase):
else:
raise ValueError("Unknown socket type.")
self.xmpp.register_plugins()
self.xmpp.process(threaded=True)
if skip:
# Clear startup stanzas

View File

@@ -57,7 +57,7 @@ class JID(object):
full, or bare.
"""
if name == 'resource':
if self._resource is None:
if self._resource is None and '/' in self._jid:
self._resource = self._jid.split('/', 1)[-1]
return self._resource or ""
elif name == 'user':
@@ -94,21 +94,15 @@ class JID(object):
elif name in ('server', 'domain', 'host'):
self.domain = value
elif name in ('full', 'jid'):
if '@' not in value:
if '/' in value:
d, r = value.split('/', 1)
object.__setattr__(self, "_resource", r)
else:
d = value
object.__setattr__(self, "_domain", d)
else:
self.reset(value)
self.reset(value)
self.regenerate()
elif name == 'bare':
if '@' in value:
u, d = value.split('@', 1)
object.__setattr__(self, "_user", u)
object.__setattr__(self, "_domain", d)
else:
object.__setattr__(self, "_user", '')
object.__setattr__(self, "_domain", value)
self.regenerate()
else:

View File

@@ -68,9 +68,6 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
for child in xml.getchildren():
output.append(tostring(child, tag_xmlns, stanza_ns, stream))
output.append(u"</%s>" % tag_name)
if xml.tail:
# If there is additional text after the element.
output.append(xml_escape(xml.tail))
elif xml.text:
# If we only have text content.
output.append(u">%s</%s>" % (xml_escape(xml.text), tag_name))

View File

@@ -786,6 +786,23 @@ class XMLStream(object):
if unhandled:
stanza.unhandled()
def _threaded_event_wrapper(self, func, args):
"""
Capture exceptions for event handlers that run
in individual threads.
Arguments:
func -- The event handler to execute.
args -- Arguments to the event handler.
"""
try:
func(*args)
except Exception as e:
error_msg = 'Error processing event handler: %s'
logging.exception(error_msg % str(func))
if hasattr(args[0], 'exception'):
args[0].exception(e)
def _event_runner(self):
"""
Process the event queue and execute handlers.
@@ -825,14 +842,18 @@ class XMLStream(object):
func, threaded, disposable = handler
try:
if threaded:
x = threading.Thread(name="Event_%s" % str(func),
target=func,
args=args)
x = threading.Thread(
name="Event_%s" % str(func),
target=self._threaded_event_wrapper,
args=(func, args))
x.start()
else:
func(*args)
except:
logging.exception('Error processing event handler: %s')
except Exception as e:
error_msg = 'Error processing event handler: %s'
logging.exception(error_msg % str(func))
if hasattr(args[0], 'exception'):
args[0].exception(e)
elif etype == 'quit':
logging.debug("Quitting event runner thread")
return False

View File

@@ -3,26 +3,126 @@ from sleekxmpp.xmlstream.jid import JID
class TestJIDClass(SleekTest):
def testJIDfromfull(self):
j = JID('user@someserver/some/resource')
self.assertEqual(j.user, 'user', "User does not match")
self.assertEqual(j.domain, 'someserver', "Domain does not match")
self.assertEqual(j.resource, 'some/resource', "Resource does not match")
self.assertEqual(j.bare, 'user@someserver', "Bare does not match")
self.assertEqual(j.full, 'user@someserver/some/resource', "Full does not match")
self.assertEqual(str(j), 'user@someserver/some/resource', "String does not match")
"""Verify that the JID class can parse and manipulate JIDs."""
def testJIDFromFull(self):
"""Test using JID of the form 'user@server/resource/with/slashes'."""
self.check_JID(JID('user@someserver/some/resource'),
'user',
'someserver',
'some/resource',
'user@someserver',
'user@someserver/some/resource',
'user@someserver/some/resource')
def testJIDchange(self):
"""Test changing JID of the form 'user@server/resource/with/slashes'"""
j = JID('user1@someserver1/some1/resource1')
j.user = 'user'
j.domain = 'someserver'
j.resource = 'some/resource'
self.assertEqual(j.user, 'user', "User does not match")
self.assertEqual(j.domain, 'someserver', "Domain does not match")
self.assertEqual(j.resource, 'some/resource', "Resource does not match")
self.assertEqual(j.bare, 'user@someserver', "Bare does not match")
self.assertEqual(j.full, 'user@someserver/some/resource', "Full does not match")
self.assertEqual(str(j), 'user@someserver/some/resource', "String does not match")
self.check_JID(j,
'user',
'someserver',
'some/resource',
'user@someserver',
'user@someserver/some/resource',
'user@someserver/some/resource')
def testJIDaliases(self):
"""Test changing JID using aliases for domain."""
j = JID('user@someserver/resource')
j.server = 'anotherserver'
self.check_JID(j, domain='anotherserver')
j.host = 'yetanother'
self.check_JID(j, domain='yetanother')
def testJIDSetFullWithUser(self):
"""Test setting the full JID with a user portion."""
j = JID('user@domain/resource')
j.full = 'otheruser@otherdomain/otherresource'
self.check_JID(j,
'otheruser',
'otherdomain',
'otherresource',
'otheruser@otherdomain',
'otheruser@otherdomain/otherresource',
'otheruser@otherdomain/otherresource')
def testJIDFullNoUserWithResource(self):
"""
Test setting the full JID without a user
portion and with a resource.
"""
j = JID('user@domain/resource')
j.full = 'otherdomain/otherresource'
self.check_JID(j,
'',
'otherdomain',
'otherresource',
'otherdomain',
'otherdomain/otherresource',
'otherdomain/otherresource')
def testJIDFullNoUserNoResource(self):
"""
Test setting the full JID without a user
portion and without a resource.
"""
j = JID('user@domain/resource')
j.full = 'otherdomain'
self.check_JID(j,
'',
'otherdomain',
'',
'otherdomain',
'otherdomain',
'otherdomain')
def testJIDBareUser(self):
"""Test setting the bare JID with a user."""
j = JID('user@domain/resource')
j.bare = 'otheruser@otherdomain'
self.check_JID(j,
'otheruser',
'otherdomain',
'resource',
'otheruser@otherdomain',
'otheruser@otherdomain/resource',
'otheruser@otherdomain/resource')
def testJIDBareNoUser(self):
"""Test setting the bare JID without a user."""
j = JID('user@domain/resource')
j.bare = 'otherdomain'
self.check_JID(j,
'',
'otherdomain',
'resource',
'otherdomain',
'otherdomain/resource',
'otherdomain/resource')
def testJIDNoResource(self):
"""Test using JID of the form 'user@domain'."""
self.check_JID(JID('user@someserver'),
'user',
'someserver',
'',
'user@someserver',
'user@someserver',
'user@someserver')
def testJIDNoUser(self):
"""Test JID of the form 'component.domain.tld'."""
self.check_JID(JID('component.someserver'),
'',
'component.someserver',
'',
'component.someserver',
'component.someserver',
'component.someserver')
suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass)

View File

@@ -56,4 +56,21 @@ class TestErrorStanzas(SleekTest):
</message>
""", use_values=False)
def testDelText(self):
"""Test deleting the text of an error."""
msg = self.Message()
msg['error']['test'] = 'Error!'
msg['error']['condition'] = 'internal-server-error'
del msg['error']['text']
self.check_message(msg, """
<message type="error">
<error type="cancel">
<internal-server-error xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
</error>
</message>
""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestErrorStanzas)

View File

@@ -0,0 +1,110 @@
import sys
import sleekxmpp
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.test import *
class TestStreamExceptions(SleekTest):
"""
Test handling roster updates.
"""
def tearDown(self):
self.stream_close()
def testXMPPErrorException(self):
"""Test raising an XMPPError exception."""
def message(msg):
raise XMPPError(condition='feature-not-implemented',
text="We don't do things that way here.",
etype='cancel',
extension='foo',
extension_ns='foo:error',
extension_args={'test': 'true'})
self.stream_start()
self.xmpp.add_event_handler('message', message)
self.stream_recv("""
<message>
<body>This is going to cause an error.</body>
</message>
""")
self.stream_send_message("""
<message type="error">
<error type="cancel">
<feature-not-implemented
xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
We don&apos;t do things that way here.
</text>
<foo xmlns="foo:error" test="true" />
</error>
</message>
""", use_values=False)
def testThreadedXMPPErrorException(self):
"""Test raising an XMPPError exception in a threaded handler."""
def message(msg):
raise XMPPError(condition='feature-not-implemented',
text="We don't do things that way here.",
etype='cancel')
self.stream_start()
self.xmpp.add_event_handler('message', message,
threaded=True)
self.stream_recv("""
<message>
<body>This is going to cause an error.</body>
</message>
""")
self.stream_send_message("""
<message type="error">
<error type="cancel">
<feature-not-implemented
xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
We don&apos;t do things that way here.
</text>
</error>
</message>
""")
def testUnknownException(self):
"""Test raising an generic exception in a threaded handler."""
def message(msg):
raise ValueError("Did something wrong")
self.stream_start()
self.xmpp.add_event_handler('message', message)
self.stream_recv("""
<message>
<body>This is going to cause an error.</body>
</message>
""")
if sys.version_info < (3, 0):
self.stream_send_message("""
<message type="error">
<error type="cancel">
<undefined-condition
xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
SleekXMPP got into trouble.
</text>
</error>
</message>
""")
else:
# Unfortunately, tracebacks do not make for very portable tests.
pass
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamExceptions)

View File

@@ -0,0 +1,188 @@
import time
from sleekxmpp.test import *
class TestStreamPresence(SleekTest):
"""
Test handling roster updates.
"""
def tearDown(self):
self.stream_close()
def testInitialUnavailablePresences(self):
"""
Test receiving unavailable presences from JIDs that
are not online.
"""
events = set()
def got_offline(presence):
# The got_offline event should not be triggered.
events.add('got_offline')
def unavailable(presence):
# The presence_unavailable event should be triggered.
events.add('unavailable')
self.stream_start()
self.xmpp.add_event_handler('got_offline', got_offline)
self.xmpp.add_event_handler('presence_unavailable', unavailable)
self.stream_recv("""
<presence type="unavailable" from="otheruser@localhost" />
""")
# Give event queue time to process.
time.sleep(0.1)
self.assertEqual(events, set(('unavailable',)),
"Got offline incorrectly triggered: %s." % events)
def testGotOffline(self):
"""Test that got_offline is triggered properly."""
events = []
def got_offline(presence):
events.append('got_offline')
self.stream_start()
self.xmpp.add_event_handler('got_offline', got_offline)
# Setup roster. Use a 'set' instead of 'result' so we
# don't have to handle get_roster() blocking.
#
# We use the stream to initialize the roster to make
# the test independent of the roster implementation.
self.stream_recv("""
<iq type="set">
<query xmlns="jabber:iq:roster">
<item jid="otheruser@localhost"
name="Other User"
subscription="both">
<group>Testers</group>
</item>
</query>
</iq>
""")
# Contact comes online.
self.stream_recv("""
<presence from="otheruser@localhost/foobar" />
""")
# Contact goes offline, should trigger got_offline.
self.stream_recv("""
<presence from="otheruser@localhost/foobar"
type="unavailable" />
""")
# Give event queue time to process.
time.sleep(0.1)
self.assertEqual(events, ['got_offline'],
"Got offline incorrectly triggered: %s" % events)
def testGotOnline(self):
"""Test that got_online is triggered properly."""
events = set()
def presence_available(p):
events.add('presence_available')
def got_online(p):
events.add('got_online')
self.stream_start()
self.xmpp.add_event_handler('presence_available', presence_available)
self.xmpp.add_event_handler('got_online', got_online)
self.stream_recv("""
<presence from="user@localhost" />
""")
# Give event queue time to process.
time.sleep(0.1)
expected = set(('presence_available', 'got_online'))
self.assertEqual(events, expected,
"Incorrect events triggered: %s" % events)
def testAutoAuthorizeAndSubscribe(self):
"""
Test auto authorizing and auto subscribing
to subscription requests.
"""
events = set()
def presence_subscribe(p):
events.add('presence_subscribe')
def changed_subscription(p):
events.add('changed_subscription')
self.stream_start(jid='tester@localhost')
self.xmpp.add_event_handler('changed_subscription',
changed_subscription)
self.xmpp.add_event_handler('presence_subscribe',
presence_subscribe)
# With these settings we should accept a subscription
# and request a subscription in return.
self.xmpp.auto_authorize = True
self.xmpp.auto_subscribe = True
self.stream_recv("""
<presence from="user@localhost" type="subscribe" />
""")
self.stream_send_presence("""
<presence to="user@localhost" type="subscribed" />
""")
self.stream_send_presence("""
<presence to="user@localhost" type="subscribe" />
""")
expected = set(('presence_subscribe', 'changed_subscription'))
self.assertEqual(events, expected,
"Incorrect events triggered: %s" % events)
def testNoAutoAuthorize(self):
"""Test auto rejecting subscription requests."""
events = set()
def presence_subscribe(p):
events.add('presence_subscribe')
def changed_subscription(p):
events.add('changed_subscription')
self.stream_start(jid='tester@localhost')
self.xmpp.add_event_handler('changed_subscription',
changed_subscription)
self.xmpp.add_event_handler('presence_subscribe',
presence_subscribe)
# With this setting we should reject all subscriptions.
self.xmpp.auto_authorize = False
self.stream_recv("""
<presence from="user@localhost" type="subscribe" />
""")
self.stream_send_presence("""
<presence to="user@localhost" type="unsubscribed" />
""")
expected = set(('presence_subscribe', 'changed_subscription'))
self.assertEqual(events, expected,
"Incorrect events triggered: %s" % events)
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamPresence)

View File

@@ -0,0 +1,86 @@
from sleekxmpp.test import *
import time
import threading
class TestStreamRoster(SleekTest):
"""
Test handling roster updates.
"""
def tearDown(self):
self.stream_close()
def testGetRoster(self):
"""Test handling roster requests."""
self.stream_start(mode='client')
self.failUnless(self.xmpp.roster == {}, "Initial roster not empty.")
# Since get_roster blocks, we need to run it in a thread.
t = threading.Thread(name='get_roster', target=self.xmpp.get_roster)
t.start()
self.stream_send_iq("""
<iq type="get" id="1">
<query xmlns="jabber:iq:roster" />
</iq>
""")
self.stream_recv("""
<iq type="result" id="1">
<query xmlns="jabber:iq:roster">
<item jid="user@localhost"
name="User"
subscription="both">
<group>Friends</group>
<group>Examples</group>
</item>
</query>
</iq>
""")
# Wait for get_roster to return.
t.join()
roster = {'user@localhost': {'name': 'User',
'subscription': 'both',
'groups': ['Friends', 'Examples'],
'presence': {},
'in_roster': True}}
self.failUnless(self.xmpp.roster == roster,
"Unexpected roster values: %s" % self.xmpp.roster)
def testRosterSet(self):
"""Test handling pushed roster updates."""
self.stream_start(mode='client')
self.failUnless(self.xmpp.roster == {}, "Initial roster not empty.")
self.stream_recv("""
<iq type="set" id="1">
<query xmlns="jabber:iq:roster">
<item jid="user@localhost"
name="User"
subscription="both">
<group>Friends</group>
<group>Examples</group>
</item>
</query>
</iq>
""")
self.stream_send_iq("""
<iq type="result" id="1">
<query xmlns="jabber:iq:roster" />
</iq>
""")
roster = {'user@localhost': {'name': 'User',
'subscription': 'both',
'groups': ['Friends', 'Examples'],
'presence': {},
'in_roster': True}}
self.failUnless(self.xmpp.roster == roster,
"Unexpected roster values: %s" % self.xmpp.roster)
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamRoster)

View File

@@ -73,6 +73,16 @@ class TestToString(SleekTest):
message="The xmlns parameter was not used properly.",
xmlns='foo')
def testTailContent(self):
"""
Test that elements of the form <a>foo <b>bar</b> baz</a> only
include " baz" once.
"""
self.tryTostring(
original='<a>foo <b>bar</b> baz</a>',
message='Element tail content is incorrect.')
def testStanzaNs(self):
"""
Test using the stanza_ns tostring parameter, which will prevent