diff options
author | Simon McVittie <simon.mcvittie@collabora.co.uk> | 2010-07-01 13:21:09 +0100 |
---|---|---|
committer | Simon McVittie <simon.mcvittie@collabora.co.uk> | 2010-07-01 13:21:13 +0100 |
commit | 70eb66f5d42033fbabfe25895216345e64cda4b4 (patch) | |
tree | 4b8c6cd21f2162669e9c28a0ddda877c8c869d63 | |
parent | 86333c7d2b313f59a22d0ef611fa6b4074b08bac (diff) | |
parent | 7f234add16a76d62c42c7616e11b3492017f2253 (diff) |
Merge branch 'contact-list'
Reviewed-by: Will Thompson <will.thompson@collabora.co.uk>
-rw-r--r-- | configure.ac | 2 | ||||
-rw-r--r-- | src/connection.c | 15 | ||||
-rw-r--r-- | src/connection.h | 2 | ||||
-rw-r--r-- | src/contact-list-channel.c | 74 | ||||
-rwxr-xr-x | tests/exec-with-log.sh | 1 | ||||
-rw-r--r-- | tests/twisted/Makefile.am | 5 | ||||
-rw-r--r-- | tests/twisted/constants.py | 37 | ||||
-rw-r--r-- | tests/twisted/hazetest.py | 86 | ||||
-rw-r--r-- | tests/twisted/ns.py | 39 | ||||
-rw-r--r-- | tests/twisted/roster/groups.py | 209 | ||||
-rw-r--r-- | tests/twisted/roster/initial-roster.py | 144 | ||||
-rw-r--r-- | tests/twisted/roster/publish.py | 137 | ||||
-rw-r--r-- | tests/twisted/roster/removed-from-rp-subscribe.py | 148 | ||||
-rw-r--r-- | tests/twisted/roster/subscribe.py | 110 | ||||
-rw-r--r-- | tests/twisted/servicetest.py | 397 | ||||
-rw-r--r-- | tests/twisted/text/destroy.py | 5 | ||||
-rw-r--r-- | tests/twisted/text/respawn.py | 11 |
17 files changed, 1128 insertions, 294 deletions
diff --git a/configure.ac b/configure.ac index f5ceeba..cf40557 100644 --- a/configure.ac +++ b/configure.ac @@ -60,6 +60,8 @@ TP_COMPILER_WARNINGS([ERROR_CFLAGS], [test x$release = xno], unused-parameter]) AC_SUBST(ERROR_CFLAGS) +AC_CHECK_HEADERS_ONCE([libintl.h]) + AC_ARG_ENABLE(leaky-request-stubs, AC_HELP_STRING([--enable-leaky-request-stubs],[print debugging information when libpurple attempts to use the request API (warning: very leaky)]), AC_DEFINE(ENABLE_LEAKY_REQUEST_STUBS, [], [Enable the leaky stub implementation of the request API for debugging purposes])) diff --git a/src/connection.c b/src/connection.c index 93f8eba..93bbaec 100644 --- a/src/connection.c +++ b/src/connection.c @@ -47,6 +47,12 @@ #include "connection-capabilities.h" +#ifdef HAVE_LIBINTL_H +# include <libintl.h> +#else +# define dgettext(domain, msgid) (msgid) +#endif + enum { PROP_PARAMETERS = 1, @@ -777,3 +783,12 @@ haze_connection_handle_inspect (HazeConnection *conn, g_assert (tp_handle_is_valid (handle_repo, handle, NULL)); return tp_handle_inspect (handle_repo, handle); } + +/** + * Get the group that "most" libpurple prpls will use for ungrouped contacts. + */ +const gchar * +haze_get_fallback_group (void) +{ + return dgettext ("pidgin", "Buddies"); +} diff --git a/src/connection.h b/src/connection.h index 630f48e..5166cdc 100644 --- a/src/connection.h +++ b/src/connection.h @@ -114,6 +114,8 @@ GType haze_connection_get_type (void); (G_TYPE_INSTANCE_GET_CLASS ((obj), HAZE_TYPE_CONNECTION, \ HazeConnectionClass)) +const gchar *haze_get_fallback_group (void); + G_END_DECLS #endif /* #ifndef __HAZE_CONNECTION_H__*/ diff --git a/src/contact-list-channel.c b/src/contact-list-channel.c index 7cf0ead..c82f099 100644 --- a/src/contact-list-channel.c +++ b/src/contact-list-channel.c @@ -347,26 +347,60 @@ _group_remove_member_cb (HazeContactListChannel *chan, const gchar *bname = haze_connection_handle_inspect (conn, TP_HANDLE_TYPE_CONTACT, handle); PurpleGroup *group = priv->group; - PurpleBuddy *buddy = purple_find_buddy_in_group (account, bname, group); - - /* FIXME: check if the buddy is in another group; if not, move it - * to the default group to avoid it falling off the subscribe list. - */ - purple_account_remove_buddy (account, buddy, group); - purple_blist_remove_buddy(buddy); - - /* Sanity checking: see if the buddy was in the group more than - * once, since this is possible in libpurple... - */ - while ((buddy = purple_find_buddy_in_group (account, bname, group))) - { - g_warning("'%s' was in group '%s' more than once! purging!", - bname, group->name); - purple_account_remove_buddy (account, buddy, group); - purple_blist_remove_buddy(buddy); - } - - return TRUE; + GSList *buddies = purple_find_buddies (account, bname); + GSList *l; + gboolean orphaned = TRUE; + gboolean ret = TRUE; + + for (l = buddies; l != NULL; l = l->next) + { + PurpleGroup *their_group = purple_buddy_get_group (l->data); + + if (their_group != group) + { + orphaned = FALSE; + break; + } + } + + if (orphaned) + { + /* the contact needs to be copied to the default group first */ + PurpleGroup *default_group = purple_group_new ( + haze_get_fallback_group ()); + PurpleBuddy *copy; + + if (default_group == group) + { + /* we could make them bounce back, but that'd be insane */ + g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Contacts can't be removed from '%s' unless they are in " + "another group", group->name); + ret = FALSE; + goto finally; + } + + copy = purple_buddy_new (conn->account, bname, NULL); + purple_blist_add_buddy (copy, NULL, default_group, NULL); + purple_account_add_buddy (account, copy); + } + + /* See if the buddy was in the group more than once, since this is + * possible in libpurple... */ + for (l = buddies; l != NULL; l = l->next) + { + PurpleGroup *their_group = purple_buddy_get_group (l->data); + + if (their_group == group) + { + purple_account_remove_buddy (account, l->data, group); + purple_blist_remove_buddy (l->data); + } + } + +finally: + g_slist_free (buddies); + return ret; } static gboolean diff --git a/tests/exec-with-log.sh b/tests/exec-with-log.sh index abfff63..fe25b3c 100755 --- a/tests/exec-with-log.sh +++ b/tests/exec-with-log.sh @@ -7,6 +7,7 @@ shift cd "${abs_top_builddir}/tests" +export LC_ALL=C export HAZE_DEBUG=all ulimit -c unlimited exec >> haze-testing.log 2>&1 diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am index 0471d24..7beaca7 100644 --- a/tests/twisted/Makefile.am +++ b/tests/twisted/Makefile.am @@ -4,6 +4,11 @@ TWISTED_TESTS = \ connect/success.py \ connect/twice-to-same-account.py \ presence/presence.py \ + roster/initial-roster.py \ + roster/groups.py \ + roster/publish.py \ + roster/removed-from-rp-subscribe.py \ + roster/subscribe.py \ text/destroy.py \ text/ensure.py \ text/initiate-requestotron.py \ diff --git a/tests/twisted/constants.py b/tests/twisted/constants.py index 07d81a3..7c57a7e 100644 --- a/tests/twisted/constants.py +++ b/tests/twisted/constants.py @@ -23,6 +23,7 @@ CHANNEL_IFACE_MEDIA_SIGNALLING = CHANNEL + ".Interface.MediaSignalling" CHANNEL_IFACE_MESSAGES = CHANNEL + ".Interface.Messages" CHANNEL_IFACE_PASSWORD = CHANNEL + ".Interface.Password" CHANNEL_IFACE_TUBE = CHANNEL + ".Interface.Tube" +CHANNEL_IFACE_SASL_AUTH = CHANNEL + ".Interface.SaslAuthentication.DRAFT" CHANNEL_TYPE_CALL = CHANNEL + ".Type.Call.DRAFT" CHANNEL_TYPE_CONTACT_LIST = CHANNEL + ".Type.ContactList" @@ -34,6 +35,8 @@ CHANNEL_TYPE_DBUS_TUBE = CHANNEL + ".Type.DBusTube" CHANNEL_TYPE_STREAMED_MEDIA = CHANNEL + ".Type.StreamedMedia" CHANNEL_TYPE_TEXT = CHANNEL + ".Type.Text" CHANNEL_TYPE_FILE_TRANSFER = CHANNEL + ".Type.FileTransfer" +CHANNEL_TYPE_SERVER_AUTHENTICATION = \ + CHANNEL + ".Type.ServerAuthentication.DRAFT" TP_AWKWARD_PROPERTIES = "org.freedesktop.Telepathy.Properties" PROPERTY_FLAG_READ = 1 @@ -123,6 +126,7 @@ CONNECTION_LOST = ERROR + '.ConnectionLost' CANCELLED = ERROR + '.Cancelled' DISCONNECTED = ERROR + '.Disconnected' REGISTRATION_EXISTS = ERROR + '.RegistrationExists' +AUTHENTICATION_FAILED = ERROR + '.AuthenticationFailed' UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod' @@ -220,6 +224,7 @@ FT_DATE = CHANNEL_TYPE_FILE_TRANSFER + '.Date' FT_AVAILABLE_SOCKET_TYPES = CHANNEL_TYPE_FILE_TRANSFER + '.AvailableSocketTypes' FT_TRANSFERRED_BYTES = CHANNEL_TYPE_FILE_TRANSFER + '.TransferredBytes' FT_INITIAL_OFFSET = CHANNEL_TYPE_FILE_TRANSFER + '.InitialOffset' +FT_FILE_COLLECTION = CHANNEL_TYPE_FILE_TRANSFER + '.FUTURE.FileCollection' GF_CAN_ADD = 1 GF_CAN_REMOVE = 2 @@ -312,3 +317,35 @@ PRESENCE_ERROR = 8 CONTACT_INFO_FLAG_CAN_SET = 1 CONTACT_INFO_FLAG_PUSH = 2 CONTACT_INFO_FIELD_FLAG_PARAMETERS_MANDATORY = 1 + +# Channel_Type_ServerAuthentication +AUTH_TYPE_SASL = 0 +AUTH_TYPE_CAPTCHA = 1 + +# Channel_Interface_SaslAuthentication +SASL_STATUS_NOT_STARTED = 0 +SASL_STATUS_IN_PROGRESS = 1 +SASL_STATUS_SERVER_SUCCEEDED = 2 +SASL_STATUS_CLIENT_ACCEPTED = 3 +SASL_STATUS_SUCCEEDED = 4 +SASL_STATUS_SERVER_FAILED = 5 +SASL_STATUS_CLIENT_FAILED = 6 + +SASL_ABORT_REASON_INVALID_CHALLENGE = 0 +SASL_ABORT_REASON_USER_ABORT = 1 + +AUTH_METHOD = CHANNEL_TYPE_SERVER_AUTHENTICATION + ".AuthenticationMethod" +AUTH_INFO = CHANNEL_TYPE_SERVER_AUTHENTICATION + ".AuthenticationInformation" +SASL_AVAILABLE_MECHANISMS = CHANNEL_IFACE_SASL_AUTH + ".AvailableMechanisms" + +# Connection.Interface.Location + +LOCATION_FEATURE_CAN_SET = 1 + +# Channel.Type.Text + +MT_NORMAL = 0 +MT_ACTION = 1 +MT_NOTICE = 2 +MT_AUTO_REPLY = 3 +MT_DELIVERY_REPORT = 4 diff --git a/tests/twisted/hazetest.py b/tests/twisted/hazetest.py index dba64d9..080e450 100644 --- a/tests/twisted/hazetest.py +++ b/tests/twisted/hazetest.py @@ -5,7 +5,7 @@ Infrastructure code for testing Haze by pretending to be a Jabber server. import base64 import os -import sha +import hashlib import sys import time import random @@ -13,6 +13,7 @@ import random import ns import servicetest import twisted +from servicetest import Event, unwrap from twisted.words.xish import domish, xpath from twisted.words.protocols.jabber.client import IQ from twisted.words.protocols.jabber import xmlstream @@ -128,7 +129,7 @@ class JabberAuthenticator(xmlstream.Authenticator): assert map(str, username) == [self.username] digest = xpath.queryForNodes('/iq/query/digest', iq) - expect = sha.sha(self.xmlstream.sid + self.password).hexdigest() + expect = hashlib.sha1(self.xmlstream.sid + self.password).hexdigest() assert map(str, digest) == [expect] resource = xpath.queryForNodes('/iq/query/resource', iq) @@ -240,9 +241,7 @@ class BaseXmlStream(xmlstream.XmlStream): self.addObserver( "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']", self._cb_disco_iq) - self.addObserver( - "/iq/query[@xmlns='jabber:iq:roster']", - self._cb_roster_get) + self.add_roster_observer() self.event_func(servicetest.Event('stream-authenticated')) def _cb_disco_iq(self, iq): @@ -259,6 +258,11 @@ class BaseXmlStream(xmlstream.XmlStream): iq['type'] = 'result' self.send(iq) + def add_roster_observer(self): + self.addObserver( + "/iq/query[@xmlns='jabber:iq:roster']", + self._cb_roster_get) + def _cb_roster_get(self, iq): # Just send back an empty roster. prpl-jabber waits for the roster # before saying it's online. @@ -303,66 +307,13 @@ def make_stream(event_func, authenticator=None, protocol=None, port=4242): port = reactor.listenTCP(port, factory) return (stream, port) -def go(params=None, authenticator=None, protocol=None, start=None): - # hack to ease debugging - domish.Element.__repr__ = domish.Element.toXml - - bus = dbus.SessionBus() - handler = servicetest.EventTest() - conn = make_connection(bus, handler.handle_event, params) - (stream, _) = make_stream(handler.handle_event, authenticator, protocol) - handler.data = { - 'bus': bus, - 'conn': conn, - 'conn_iface': dbus.Interface(conn, - 'org.freedesktop.Telepathy.Connection'), - 'stream': stream} - handler.data['test'] = handler - handler.verbose = (os.environ.get('CHECK_TWISTED_VERBOSE', '') != '') - map(handler.expect, servicetest.load_event_handlers()) - - if '-v' in sys.argv: - handler.verbose = True - - if start is None: - handler.data['conn'].Connect() - else: - start(handler.data) - - reactor.run() - -def install_colourer(): - def red(s): - return '\x1b[31m%s\x1b[0m' % s - - def green(s): - return '\x1b[32m%s\x1b[0m' % s - - patterns = { - 'handled': green, - 'not handled': red, - } - - class Colourer: - def __init__(self, fh, patterns): - self.fh = fh - self.patterns = patterns - - def write(self, s): - f = self.patterns.get(s, lambda x: x) - self.fh.write(f(s)) - - sys.stdout = Colourer(sys.stdout, patterns) - return sys.stdout - - def exec_test_deferred (funs, params, protocol=None, timeout=None): # hack to ease debugging domish.Element.__repr__ = domish.Element.toXml colourer = None if sys.stdout.isatty(): - colourer = install_colourer() + colourer = servicetest.install_colourer() queue = servicetest.IteratingEventQueue(timeout) queue.verbose = ( @@ -373,6 +324,23 @@ def exec_test_deferred (funs, params, protocol=None, timeout=None): # conn = make_connection(bus, queue.append, params) (stream, port) = make_stream(queue.append, protocol=protocol) + def signal_receiver(*args, **kw): + queue.append(Event('dbus-signal', + path=unwrap(kw['path']), + signal=kw['member'], args=map(unwrap, args), + interface=kw['interface'])) + + bus.add_signal_receiver( + signal_receiver, + None, # signal name + None, # interface + None, + path_keyword='path', + member_keyword='member', + interface_keyword='interface', + byte_arrays=True + ) + error = None try: diff --git a/tests/twisted/ns.py b/tests/twisted/ns.py index 143aa1c..d9b290f 100644 --- a/tests/twisted/ns.py +++ b/tests/twisted/ns.py @@ -5,16 +5,38 @@ CAPS = "http://jabber.org/protocol/caps" DISCO_INFO = "http://jabber.org/protocol/disco#info" DISCO_ITEMS = "http://jabber.org/protocol/disco#items" FEATURE_NEG = 'http://jabber.org/protocol/feature-neg' +FILE_TRANSFER = 'http://jabber.org/protocol/si/profile/file-transfer' +GEOLOC = 'http://jabber.org/protocol/geoloc' +GOOGLE_FEAT_SESSION = 'http://www.google.com/xmpp/protocol/session' +GOOGLE_FEAT_SHARE = 'http://google.com/xmpp/protocol/share/v1' +GOOGLE_FEAT_VOICE = 'http://www.google.com/xmpp/protocol/voice/v1' +GOOGLE_FEAT_VIDEO = 'http://www.google.com/xmpp/protocol/video/v1' GOOGLE_JINGLE_INFO = 'google:jingleinfo' GOOGLE_P2P = "http://www.google.com/transport/p2p" +GOOGLE_QUEUE = 'google:queue' GOOGLE_ROSTER = 'google:roster' +GOOGLE_SESSION = "http://www.google.com/session" +GOOGLE_SESSION_SHARE = "http://www.google.com/session/share" +GOOGLE_SESSION_PHONE = "http://www.google.com/session/phone" +GOOGLE_SESSION_VIDEO = "http://www.google.com/session/video" +GOOGLE_MAIL_NOTIFY = "google:mail:notify" IBB = 'http://jabber.org/protocol/ibb' -JINGLE = "http://jabber.org/protocol/jingle" -JINGLE_AUDIO = "http://jabber.org/protocol/jingle/description/audio" +JINGLE_015 = "http://jabber.org/protocol/jingle" +JINGLE_015_AUDIO = "http://jabber.org/protocol/jingle/description/audio" +JINGLE_015_VIDEO = "http://jabber.org/protocol/jingle/description/video" +JINGLE = "urn:xmpp:jingle:1" +JINGLE_RTP = "urn:xmpp:jingle:apps:rtp:1" +JINGLE_RTP_AUDIO = "urn:xmpp:jingle:apps:rtp:audio" +JINGLE_RTP_VIDEO = "urn:xmpp:jingle:apps:rtp:video" +JINGLE_RTP_ERRORS = "urn:xmpp:jingle:apps:rtp:errors:1" +JINGLE_RTP_INFO_1 = "urn:xmpp:jingle:apps:rtp:info:1" +JINGLE_TRANSPORT_ICEUDP = "urn:xmpp:jingle:transports:ice-udp:1" +JINGLE_TRANSPORT_RAWUDP = "urn:xmpp:jingle:transports:raw-udp:1" MUC = 'http://jabber.org/protocol/muc' MUC_BYTESTREAM = 'http://telepathy.freedesktop.org/xmpp/protocol/muc-bytestream' MUC_OWNER = '%s#owner' % MUC MUC_USER = '%s#user' % MUC +NICK = "http://jabber.org/protocol/nick" OLPC_ACTIVITIES = "http://laptop.org/xmpp/activities" OLPC_ACTIVITIES_NOTIFY = "%s+notify" % OLPC_ACTIVITIES OLPC_ACTIVITY = "http://laptop.org/xmpp/activity" @@ -26,8 +48,21 @@ OLPC_BUDDY_PROPS_NOTIFY = "%s+notify" % OLPC_BUDDY_PROPS OLPC_CURRENT_ACTIVITY = "http://laptop.org/xmpp/current-activity" OLPC_CURRENT_ACTIVITY_NOTIFY = "%s+notify" % OLPC_CURRENT_ACTIVITY PUBSUB = "http://jabber.org/protocol/pubsub" +PUBSUB_EVENT = "%s#event" % PUBSUB +REGISTER = "jabber:iq:register" +ROSTER = "jabber:iq:roster" +SEARCH = 'jabber:iq:search' SI = 'http://jabber.org/protocol/si' SI_MULTIPLE = 'http://telepathy.freedesktop.org/xmpp/si-multiple' STANZA = "urn:ietf:params:xml:ns:xmpp-stanzas" +STREAMS = "urn:ietf:params:xml:ns:xmpp-streams" +TEMPPRES = "urn:xmpp:temppres:0" TUBES = 'http://telepathy.freedesktop.org/xmpp/tubes' +MUJI = 'http://telepathy.freedesktop.org/xmpp/muji' +VCARD_TEMP = 'vcard-temp' +VCARD_TEMP_UPDATE = 'vcard-temp:x:update' X_DATA = 'jabber:x:data' +X_DELAY = 'jabber:x:delay' +XML = 'http://www.w3.org/XML/1998/namespace' +X_OOB = 'jabber:x:oob' +GABBLE_CAPS="http://telepathy.freedesktop.org/caps" diff --git a/tests/twisted/roster/groups.py b/tests/twisted/roster/groups.py new file mode 100644 index 0000000..4702f2e --- /dev/null +++ b/tests/twisted/roster/groups.py @@ -0,0 +1,209 @@ +""" +Test adding to, and removing from, groups +""" + +import dbus + +from twisted.words.protocols.jabber.client import IQ +from twisted.words.xish import domish, xpath + +from servicetest import (EventPattern, wrap_channel, assertLength, + assertEquals, call_async, sync_dbus, assertContains) +from hazetest import acknowledge_iq, exec_test, sync_stream +import constants as cs +import ns + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + call_async(q, conn.Requests, 'EnsureChannel',{ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + cs.TARGET_HANDLE_TYPE: cs.HT_LIST, + cs.TARGET_ID: 'subscribe', + }) + e = q.expect('dbus-return', method='EnsureChannel') + subscribe = wrap_channel(bus.get_object(conn.bus_name, e.value[1]), + cs.CHANNEL_TYPE_CONTACT_LIST) + + romeo, juliet, duncan = conn.RequestHandles(cs.HT_CONTACT, + ['romeo@montague.lit', 'juliet@capulet.lit', + 'duncan@scotland.lit']) + + # receive some roster pushes for the "initial" state + iq = IQ(stream, 'set') + iq['id'] = 'roster-push' + query = iq.addElement(('jabber:iq:roster', 'query')) + item = query.addElement('item') + item['jid'] = 'juliet@capulet.lit' + item['subscription'] = 'both' + group = item.addElement('group', content='Still alive') + group = item.addElement('group', content='Capulets') + stream.send(iq) + + iq = IQ(stream, 'set') + iq['id'] = 'roster-push' + query = iq.addElement(('jabber:iq:roster', 'query')) + item = query.addElement('item') + item['jid'] = 'romeo@montague.lit' + item['subscription'] = 'both' + group = item.addElement('group', content='Still alive') + stream.send(iq) + + iq = IQ(stream, 'set') + iq['id'] = 'roster-push' + query = iq.addElement(('jabber:iq:roster', 'query')) + item = query.addElement('item') + item['jid'] = 'duncan@scotland.lit' + item['subscription'] = 'both' + stream.send(iq) + + sync_dbus(bus, q, conn) + sync_stream(q, stream) + + call_async(q, conn.Requests, 'EnsureChannel',{ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + cs.TARGET_HANDLE_TYPE: cs.HT_GROUP, + cs.TARGET_ID: 'Still alive', + }) + e = q.expect('dbus-return', method='EnsureChannel') + still_alive = wrap_channel(bus.get_object(conn.bus_name, e.value[1]), + cs.CHANNEL_TYPE_CONTACT_LIST) + + call_async(q, conn.Requests, 'EnsureChannel',{ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + cs.TARGET_HANDLE_TYPE: cs.HT_GROUP, + cs.TARGET_ID: 'Capulets', + }) + e = q.expect('dbus-return', method='EnsureChannel') + capulets = wrap_channel(bus.get_object(conn.bus_name, e.value[1]), + cs.CHANNEL_TYPE_CONTACT_LIST) + + # the XMPP prpl puts people into some sort of group, probably called + # Buddies + channels = conn.Properties.Get(cs.CONN_IFACE_REQUESTS, 'Channels') + default_group = None + default_props = None + + for path, props in channels: + if props.get(cs.CHANNEL_TYPE) != cs.CHANNEL_TYPE_CONTACT_LIST: + continue + + if props.get(cs.TARGET_HANDLE_TYPE) != cs.HT_GROUP: + continue + + if props.get(cs.TARGET_ID) in ('Capulets', 'Still alive'): + continue + + if default_group is not None: + raise AssertionError('Two unexplained groups: %s, %s' % + (path, default_group.object_path)) + + default_group = wrap_channel(bus.get_object(conn.bus_name, path), + cs.CHANNEL_TYPE_CONTACT_LIST) + default_group_name = props.get(cs.TARGET_ID) + + assertEquals(set([romeo, juliet]), set(still_alive.Group.GetMembers())) + assertEquals(set([juliet]), set(capulets.Group.GetMembers())) + assertEquals(set([duncan]), set(default_group.Group.GetMembers())) + + # We can't remove Duncan from the default group, because it's his only + # group + call_async(q, default_group.Group, 'RemoveMembers', [duncan], '') + q.expect('dbus-error', method='RemoveMembers', + name=cs.NOT_AVAILABLE) + + # Make a new group and add Duncan to it + call_async(q, conn.Requests, 'CreateChannel',{ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + cs.TARGET_HANDLE_TYPE: cs.HT_GROUP, + cs.TARGET_ID: 'Scots', + }) + e = q.expect('dbus-return', method='CreateChannel') + scots = wrap_channel(bus.get_object(conn.bus_name, e.value[0]), + cs.CHANNEL_TYPE_CONTACT_LIST) + assertEquals(set(), set(scots.Group.GetMembers())) + + call_async(q, scots.Group, 'AddMembers', [duncan], '') + iq, _, _ = q.expect_many( + EventPattern('stream-iq', iq_type='set', query_name='query', + query_ns=ns.ROSTER), + EventPattern('dbus-signal', signal='MembersChanged', + path=scots.object_path, + args=['', [duncan], [], [], [], 0, cs.GC_REASON_NONE]), + EventPattern('dbus-return', method='AddMembers'), + ) + assertEquals('duncan@scotland.lit', iq.stanza.query.item['jid']) + groups = set([str(x) for x in xpath.queryForNodes('/iq/query/item/group', + iq.stanza)]) + assertLength(2, groups) + assertContains(default_group_name, groups) + assertContains('Scots', groups) + + # Now we can remove him from the default group. Much rejoicing. + call_async(q, default_group.Group, 'RemoveMembers', [duncan], '') + iq, _, _ = q.expect_many( + EventPattern('stream-iq', iq_type='set', query_name='query', + query_ns=ns.ROSTER), + EventPattern('dbus-signal', signal='MembersChanged', + path=default_group.object_path, + args=['', [], [duncan], [], [], 0, cs.GC_REASON_NONE]), + EventPattern('dbus-return', method='RemoveMembers'), + ) + assertEquals('duncan@scotland.lit', iq.stanza.query.item['jid']) + groups = set([str(x) for x in xpath.queryForNodes('/iq/query/item/group', + iq.stanza)]) + assertLength(1, groups) + assertContains('Scots', groups) + + # Romeo dies. If he drops off the roster as a result, that would be + # fd.o #21294. However, to fix that bug, Haze now puts him in the + # default group. + call_async(q, still_alive.Group, 'RemoveMembers', [romeo], '') + iq1, iq2, _, _, _ = q.expect_many( + EventPattern('stream-iq', iq_type='set', query_name='query', + query_ns=ns.ROSTER), + EventPattern('stream-iq', iq_type='set', query_name='query', + query_ns=ns.ROSTER), + EventPattern('dbus-signal', signal='MembersChanged', + path=still_alive.object_path, + args=['', [], [romeo], [], [], 0, cs.GC_REASON_NONE]), + EventPattern('dbus-signal', signal='MembersChanged', + path=default_group.object_path, + args=['', [romeo], [], [], [], 0, cs.GC_REASON_NONE]), + EventPattern('dbus-return', method='RemoveMembers'), + ) + + assertEquals('romeo@montague.lit', iq1.stanza.query.item['jid']) + groups = set([str(x) for x in xpath.queryForNodes('/iq/query/item/group', + iq1.stanza)]) + assertLength(2, groups) + assertContains('Still alive', groups) + assertContains(default_group_name, groups) + + assertEquals('romeo@montague.lit', iq2.stanza.query.item['jid']) + groups = set([str(x) for x in xpath.queryForNodes('/iq/query/item/group', + iq2.stanza)]) + assertLength(1, groups) + assertContains(default_group_name, groups) + + # Juliet dies. She's in another group already, so the workaround for + # fd.o #21294 is not active. + call_async(q, still_alive.Group, 'RemoveMembers', [juliet], '') + iq, _, _ = q.expect_many( + EventPattern('stream-iq', iq_type='set', query_name='query', + query_ns=ns.ROSTER), + EventPattern('dbus-signal', signal='MembersChanged', + path=still_alive.object_path, + args=['', [], [juliet], [], [], 0, cs.GC_REASON_NONE]), + EventPattern('dbus-return', method='RemoveMembers'), + ) + assertEquals('juliet@capulet.lit', iq.stanza.query.item['jid']) + groups = set([str(x) for x in xpath.queryForNodes('/iq/query/item/group', + iq.stanza)]) + assertLength(1, groups) + assertContains('Capulets', groups) + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/roster/initial-roster.py b/tests/twisted/roster/initial-roster.py new file mode 100644 index 0000000..c73fb1a --- /dev/null +++ b/tests/twisted/roster/initial-roster.py @@ -0,0 +1,144 @@ +""" +Test basic roster functionality. +""" + +import dbus + +from hazetest import exec_test, JabberXmlStream +from servicetest import (assertLength, EventPattern, wrap_channel, + assertEquals, call_async) +import constants as cs +import ns + +class RosterXmlStream(JabberXmlStream): + def add_roster_observer(self): + # don't wait for the roster IQ before continuing into the test + pass + +def test(q, bus, conn, stream): + conn.Connect() + + # This test can't be exactly like Gabble's because libpurple doesn't + # signal that it's connected until it receives a roster; as a result, + # the publish and subscribe channels already exist on startup. + + q.expect_many( + EventPattern('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTING, cs.CSR_REQUESTED]), + EventPattern('stream-authenticated'), + ) + + event = q.expect('stream-iq', query_ns=ns.ROSTER) + event.stanza['type'] = 'result' + + item = event.query.addElement('item') + item['jid'] = 'amy@foo.com' + item['subscription'] = 'both' + group = item.addElement('group', content='3 letter names') + + item = event.query.addElement('item') + item['jid'] = 'bob@foo.com' + item['subscription'] = 'from' + group = item.addElement('group', content='3 letter names') + + item = event.query.addElement('item') + item['jid'] = 'chris@foo.com' + item['subscription'] = 'to' + + stream.send(event.stanza) + + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + # there is no 'stored' yet; when it exists, it should have Amy, Bob and + # Chris + #call_async(q, conn.Requests, 'EnsureChannel',{ + # cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + # cs.TARGET_HANDLE_TYPE: cs.HT_LIST, + # cs.TARGET_ID: 'stored', + # }) + #e = q.expect('dbus-return', method='EnsureChannel') + #stored = wrap_channel(bus.get_object(conn.bus_name, e.value[1]), + # cs.CHANNEL_TYPE_CONTACT_LIST) + #jids = set(conn.InspectHandles(cs.HT_CONTACT, stored.Group.GetMembers())) + #assertEquals(set(['amy@foo.com', 'bob@foo.com', 'chris@foo.com']), jids) + + call_async(q, conn.Requests, 'EnsureChannel',{ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + cs.TARGET_HANDLE_TYPE: cs.HT_LIST, + cs.TARGET_ID: 'subscribe', + }) + e = q.expect('dbus-return', method='EnsureChannel') + subscribe = wrap_channel(bus.get_object(conn.bus_name, e.value[1]), + cs.CHANNEL_TYPE_CONTACT_LIST) + jids = set(conn.InspectHandles(cs.HT_CONTACT, subscribe.Group.GetMembers())) + # everyone on our roster is (falsely!) alleged to be on 'subscribe' + # (in fact this ought to be just Amy and Chris, but libpurple apparently + # can't represent this) + assertEquals(set(['amy@foo.com', 'bob@foo.com', 'chris@foo.com']), jids) + + call_async(q, conn.Requests, 'EnsureChannel',{ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + cs.TARGET_HANDLE_TYPE: cs.HT_LIST, + cs.TARGET_ID: 'publish', + }) + e = q.expect('dbus-return', method='EnsureChannel') + publish = wrap_channel(bus.get_object(conn.bus_name, e.value[1]), + cs.CHANNEL_TYPE_CONTACT_LIST) + jids = set(conn.InspectHandles(cs.HT_CONTACT, publish.Group.GetMembers())) + # the publish list is somewhat imaginary because libpurple doesn't have + # state-recovery + assertEquals(set(), jids) + + call_async(q, conn.Requests, 'EnsureChannel',{ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + cs.TARGET_HANDLE_TYPE: cs.HT_GROUP, + cs.TARGET_ID: '3 letter names', + }) + e = q.expect('dbus-return', method='EnsureChannel') + group_chan = wrap_channel(bus.get_object(conn.bus_name, e.value[1]), + cs.CHANNEL_TYPE_CONTACT_LIST) + jids = set(conn.InspectHandles(cs.HT_CONTACT, + group_chan.Group.GetMembers())) + assertEquals(set(['amy@foo.com', 'bob@foo.com']), jids) + + # the XMPP prpl puts people into some sort of group, probably called + # Buddies + channels = conn.Properties.Get(cs.CONN_IFACE_REQUESTS, 'Channels') + default_group = None + default_props = None + + for path, props in channels: + if props.get(cs.CHANNEL_TYPE) != cs.CHANNEL_TYPE_CONTACT_LIST: + continue + + if props.get(cs.TARGET_HANDLE_TYPE) != cs.HT_GROUP: + continue + + if path == group_chan.object_path: + continue + + if default_group is not None: + raise AssertionError('Two unexplained groups: %s, %s' % + (path, default_group.object_path)) + + default_group = wrap_channel(bus.get_object(conn.bus_name, path), + cs.CHANNEL_TYPE_CONTACT_LIST) + default_props = props + + jids = set(conn.InspectHandles(cs.HT_CONTACT, + default_group.Group.GetMembers())) + assertEquals(set(['chris@foo.com']), jids) + + call_async(q, conn.Requests, 'EnsureChannel',{ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + cs.TARGET_HANDLE_TYPE: cs.HT_GROUP, + cs.TARGET_ID: default_props[cs.TARGET_ID], + }) + e = q.expect('dbus-return', method='EnsureChannel') + assertEquals(False, e.value[0]) + assertEquals(default_group.object_path, e.value[1]) + assertEquals(default_props, e.value[2]) + +if __name__ == '__main__': + exec_test(test, protocol=RosterXmlStream) diff --git a/tests/twisted/roster/publish.py b/tests/twisted/roster/publish.py new file mode 100644 index 0000000..afdec3f --- /dev/null +++ b/tests/twisted/roster/publish.py @@ -0,0 +1,137 @@ +""" +Test requests to see our presence. +""" + +import dbus + +from twisted.words.protocols.jabber.client import IQ +from twisted.words.xish import domish + +from servicetest import (EventPattern, wrap_channel, assertLength, + assertEquals, call_async, sync_dbus) +from hazetest import acknowledge_iq, exec_test, sync_stream +import constants as cs +import ns + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + call_async(q, conn.Requests, 'EnsureChannel',{ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + cs.TARGET_HANDLE_TYPE: cs.HT_LIST, + cs.TARGET_ID: 'publish', + }) + e = q.expect('dbus-return', method='EnsureChannel') + publish = wrap_channel(bus.get_object(conn.bus_name, e.value[1]), + cs.CHANNEL_TYPE_CONTACT_LIST) + jids = set(conn.InspectHandles(cs.HT_CONTACT, publish.Group.GetMembers())) + assertEquals(set(), jids) + + call_async(q, conn.Requests, 'EnsureChannel',{ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + cs.TARGET_HANDLE_TYPE: cs.HT_LIST, + cs.TARGET_ID: 'subscribe', + }) + e = q.expect('dbus-return', method='EnsureChannel') + subscribe = wrap_channel(bus.get_object(conn.bus_name, e.value[1]), + cs.CHANNEL_TYPE_CONTACT_LIST) + jids = set(conn.InspectHandles(cs.HT_CONTACT, subscribe.Group.GetMembers())) + assertEquals(set(), jids) + + # receive a subscription request + alice = conn.RequestHandles(cs.HT_CONTACT, ['alice@wonderland.lit'])[0] + + presence = domish.Element(('jabber:client', 'presence')) + presence['from'] = 'alice@wonderland.lit' + presence['type'] = 'subscribe' + presence.addElement('status', content='friend me') + stream.send(presence) + + # it seems either libpurple or haze doesn't pass the message through + q.expect('dbus-signal', path=publish.object_path, + args=['', [], [], [alice], [], alice, + cs.GC_REASON_NONE]) + + # accept + call_async(q, publish.Group, 'AddMembers', [alice], '') + + q.expect_many( + EventPattern('stream-presence', presence_type='subscribed', + to='alice@wonderland.lit'), + EventPattern('dbus-signal', signal='MembersChanged', + path=publish.object_path, + args=['', [alice], [], [], [], conn.GetSelfHandle(), + cs.GC_REASON_NONE]), + EventPattern('dbus-return', method='AddMembers'), + ) + + # the server sends us a roster push + iq = IQ(stream, 'set') + iq['id'] = 'roster-push' + query = iq.addElement(('jabber:iq:roster', 'query')) + item = query.addElement('item') + item['jid'] = 'alice@wonderland.lit' + item['subscription'] = 'from' + + stream.send(iq) + + _, _, new_group = q.expect_many( + EventPattern('stream-iq', iq_type='result', + predicate=lambda e: e.stanza['id'] == 'roster-push'), + # this isn't really true, but it's the closest we can guess from + # libpurple + EventPattern('dbus-signal', signal='MembersChanged', + path=subscribe.object_path, + args=['', [alice], [], [], [], 0, cs.GC_REASON_NONE]), + # the buddy needs a group, because libpurple + EventPattern('dbus-signal', signal='NewChannels', + predicate=lambda e: + e.args[0][0][1].get(cs.CHANNEL_TYPE) == + cs.CHANNEL_TYPE_CONTACT_LIST and + e.args[0][0][1].get(cs.TARGET_HANDLE_TYPE) == + cs.HT_GROUP), + ) + + def_group = wrap_channel(bus.get_object(conn.bus_name, + new_group.args[0][0][0]), cs.CHANNEL_TYPE_CONTACT_LIST) + + assertEquals(set([alice]), set(def_group.Group.GetMembers())) + + # receive another subscription request + queen = conn.RequestHandles(cs.HT_CONTACT, + ['queen.of.hearts@wonderland.lit'])[0] + + presence = domish.Element(('jabber:client', 'presence')) + presence['from'] = 'queen.of.hearts@wonderland.lit' + presence['type'] = 'subscribe' + presence.addElement('status', content='Off with her head!') + stream.send(presence) + + # it seems either libpurple or haze doesn't pass the message through + q.expect('dbus-signal', path=publish.object_path, + args=['', [], [], [queen], [], queen, + cs.GC_REASON_NONE]) + + # decline + call_async(q, publish.Group, 'RemoveMembers', [queen], '') + + q.expect_many( + EventPattern('stream-presence', presence_type='unsubscribed', + to='queen.of.hearts@wonderland.lit'), + EventPattern('dbus-signal', signal='MembersChanged', + path=publish.object_path, + args=['', [], [queen], [], [], conn.GetSelfHandle(), + cs.GC_REASON_NONE]), + EventPattern('dbus-return', method='RemoveMembers'), + ) + + sync_dbus(bus, q, conn) + sync_stream(q, stream) + + # the declined contact isn't on our roster + assertEquals(set([alice]), set(def_group.Group.GetMembers())) + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/roster/removed-from-rp-subscribe.py b/tests/twisted/roster/removed-from-rp-subscribe.py new file mode 100644 index 0000000..a399063 --- /dev/null +++ b/tests/twisted/roster/removed-from-rp-subscribe.py @@ -0,0 +1,148 @@ +""" +Regression tests for rescinding outstanding subscription requests. +""" + +from twisted.words.protocols.jabber.client import IQ + +from servicetest import (EventPattern, wrap_channel, assertLength, + assertEquals, call_async, sync_dbus) +from hazetest import exec_test +import constants as cs +import ns + +jid = 'marco@barisione.lit' + +def test(q, bus, conn, stream, remove, local): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + call_async(q, conn.Requests, 'EnsureChannel',{ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + cs.TARGET_HANDLE_TYPE: cs.HT_LIST, + cs.TARGET_ID: 'subscribe', + }) + e = q.expect('dbus-return', method='EnsureChannel') + subscribe = wrap_channel(bus.get_object(conn.bus_name, e.value[1]), + cs.CHANNEL_TYPE_CONTACT_LIST) + + call_async(q, conn.Requests, 'EnsureChannel',{ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + cs.TARGET_HANDLE_TYPE: cs.HT_LIST, + cs.TARGET_ID: 'publish', + }) + e = q.expect('dbus-return', method='EnsureChannel') + publish = wrap_channel(bus.get_object(conn.bus_name, e.value[1]), + cs.CHANNEL_TYPE_CONTACT_LIST) + + h = conn.RequestHandles(cs.HT_CONTACT, [jid])[0] + + # Another client logged into our account (Gajim, say) wants to subscribe to + # Marco's presence. First, per RFC 3921 it 'SHOULD perform a "roster set" + # for the new roster item': + # + # <iq type='set'> + # <query xmlns='jabber:iq:roster'> + # <item jid='marco@barisione.lit'/> + # </query> + # </iq> + # + # 'As a result, the user's server (1) MUST initiate a roster push for the + # new roster item to all available resources associated with this user that + # have requested the roster, setting the 'subscription' attribute to a + # value of "none"': + iq = IQ(stream, "set") + item = iq.addElement((ns.ROSTER, 'query')).addElement('item') + item['jid'] = jid + item['subscription'] = 'none' + stream.send(iq) + + # In response, Haze adds Marco to the roster, which we guess (wrongly, + # in this case) means subscribe + q.expect('dbus-signal', signal='MembersChanged', + args=['', [h], [], [], [], 0, 0], path=subscribe.object_path) + + # Gajim sends a <presence type='subscribe'/> to Marco. 'As a result, the + # user's server MUST initiate a second roster push to all of the user's + # available resources that have requested the roster, setting [...] + # ask='subscribe' attribute in the roster item [for Marco]: + iq = IQ(stream, "set") + item = iq.addElement((ns.ROSTER, 'query')).addElement('item') + item['jid'] = jid + item['subscription'] = 'none' + item['ask'] = 'subscribe' + stream.send(iq) + + # In response, Haze should add Marco to subscribe:remote-pending, + # but libpurple has no such concept, so nothing much happens. + + # The user decides that they don't care what Marco's baking after all + # (maybe they read his blog instead?) and: + if remove: + # ...removes him from the roster... + if local: + # ...by telling Haze to remove him from subscribe (which is + # really more like stored) + subscribe.Group.RemoveMembers([h], '') + + event = q.expect('stream-iq', iq_type='set', query_ns=ns.ROSTER) + item = event.query.firstChildElement() + assertEquals(jid, item['jid']) + assertEquals('remove', item['subscription']) + else: + # ...using the other client. + pass + + # The server must 'inform all of the user's available resources that + # have requested the roster of the roster item removal': + iq = IQ(stream, "set") + item = iq.addElement((ns.ROSTER, 'query')).addElement('item') + item['jid'] = jid + item['subscription'] = 'remove' + # When Marco found this bug, this roster update included: + item['ask'] = 'subscribe' + # which is a bit weird: I don't think the server should send that when + # the contact's being removed. I think CMs should ignore it, so I'm + # including it in the test. + stream.send(iq) + + # In response, Haze should announce that Marco has been removed from + # subscribe:remote-pending and stored:members: but it has no stored + # channel. + q.expect_many( + EventPattern('dbus-signal', signal='MembersChanged', + args=['', [], [h], [], [], 0, 0], + path=subscribe.object_path), + ) + else: + # ...rescinds the subscription request... + if local: + raise AssertionError("Haze can't do this ") + else: + # ...in the other client. + pass + + # In response, the server sends a roster update: + iq = IQ(stream, "set") + item = iq.addElement((ns.ROSTER, 'query')).addElement('item') + item['jid'] = jid + item['subscription'] = 'none' + # no ask='subscribe' any more. + stream.send(iq) + + # In response, Haze should announce that Marco has been removed from + # subscribe:remote-pending; but it can't know that, so nothing happens. + +def test_remove_local(q, bus, conn, stream): + test(q, bus, conn, stream, remove=True, local=True) + +def test_remove_remote(q, bus, conn, stream): + test(q, bus, conn, stream, remove=True, local=False) + +def test_unsubscribe_remote(q, bus, conn, stream): + test(q, bus, conn, stream, remove=False, local=False) + +if __name__ == '__main__': + exec_test(test_remove_local) + exec_test(test_remove_remote) + exec_test(test_unsubscribe_remote) diff --git a/tests/twisted/roster/subscribe.py b/tests/twisted/roster/subscribe.py new file mode 100644 index 0000000..8c718c4 --- /dev/null +++ b/tests/twisted/roster/subscribe.py @@ -0,0 +1,110 @@ +""" +Test subscribing to a contact's presence. +""" + +import dbus + +from twisted.words.xish import domish + +from servicetest import (EventPattern, wrap_channel, assertLength, + assertEquals, call_async, sync_dbus) +from hazetest import acknowledge_iq, exec_test, sync_stream +import constants as cs +import ns + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + call_async(q, conn.Requests, 'EnsureChannel',{ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST, + cs.TARGET_HANDLE_TYPE: cs.HT_LIST, + cs.TARGET_ID: 'subscribe', + }) + e = q.expect('dbus-return', method='EnsureChannel') + subscribe = wrap_channel(bus.get_object(conn.bus_name, e.value[1]), + cs.CHANNEL_TYPE_CONTACT_LIST) + jids = set(conn.InspectHandles(cs.HT_CONTACT, subscribe.Group.GetMembers())) + assertEquals(set(), jids) + + assertLength(0, subscribe.Group.GetMembers()) + + # request subscription + handle = conn.RequestHandles(cs.HT_CONTACT, ['suggs@night.boat.cairo'])[0] + call_async(q, subscribe.Group, 'AddMembers', [handle], '') + + # libpurple puts him on our blist as soon as we've asked; there doesn't + # seem to be any concept of remote-pending state. + # + # It also puts him in the default group, probably "Buddies". + set_iq, _, _, _, new_channels = q.expect_many( + EventPattern('stream-iq', iq_type='set', + query_ns=ns.ROSTER, query_name='query'), + EventPattern('stream-presence', presence_type='subscribe', + to='suggs@night.boat.cairo'), + EventPattern('dbus-return', method='AddMembers', value=()), + EventPattern('dbus-signal', signal='MembersChanged', + path=subscribe.object_path, + args=['', [handle], [], [], [], 0, 0]), + EventPattern('dbus-signal', signal='NewChannels', + predicate=lambda e: + e.args[0][0][1].get(cs.TARGET_HANDLE_TYPE) == cs.HT_GROUP), + ) + + assertEquals('suggs@night.boat.cairo', set_iq.query.item['jid']) + acknowledge_iq(stream, set_iq.stanza) + + # Suggs accepts our subscription request + presence = domish.Element(('jabber:client', 'presence')) + presence['from'] = 'suggs@night.boat.cairo' + presence['type'] = 'subscribed' + stream.send(presence) + + # ... but nothing much happens, because there's no concept of pending + # state in libpurple + + def_group = wrap_channel(bus.get_object(conn.bus_name, + new_channels.args[0][0][0]), cs.CHANNEL_TYPE_CONTACT_LIST) + handles = set(subscribe.Group.GetMembers()) + assertEquals(set([handle]), handles) + + # put a contact into the *group* explicitly: this shouldn't ask for + # subscription, but it does + handle = conn.RequestHandles(cs.HT_CONTACT, ['ayria@revenge.world'])[0] + call_async(q, def_group.Group, 'AddMembers', [handle], '') + + # libpurple puts her on our blist as soon as we've asked; there doesn't + # seem to be any concept of remote-pending state. It also puts her in the + # same group. + set_iq, _, _, _, _ = q.expect_many( + EventPattern('stream-iq', iq_type='set', + query_ns=ns.ROSTER, query_name='query'), + EventPattern('stream-presence', presence_type='subscribe', + to='ayria@revenge.world'), + EventPattern('dbus-return', method='AddMembers', value=()), + EventPattern('dbus-signal', signal='MembersChanged', + path=subscribe.object_path, + args=['', [handle], [], [], [], 0, 0]), + EventPattern('dbus-signal', signal='MembersChanged', + path=def_group.object_path, + args=['', [handle], [], [], [], 0, 0]), + ) + + acknowledge_iq(stream, set_iq.stanza) + assertEquals('ayria@revenge.world', set_iq.query.item['jid']) + + # cybergoths are less receptive to random subscription requests, so it + # gets rejected + presence = domish.Element(('jabber:client', 'presence')) + presence['from'] = 'ayria@revenge.world' + presence['type'] = 'unsubscribed' + stream.send(presence) + + # nothing happens, because there's no concept of pending state... + + sync_dbus(bus, q, conn) + sync_stream(q, stream) + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/servicetest.py b/tests/twisted/servicetest.py index b8485e1..bb00d7a 100644 --- a/tests/twisted/servicetest.py +++ b/tests/twisted/servicetest.py @@ -6,50 +6,20 @@ Infrastructure code for testing connection managers. from twisted.internet import glib2reactor from twisted.internet.protocol import Protocol, Factory, ClientFactory glib2reactor.install() +import sys import pprint -import traceback import unittest import dbus.glib from twisted.internet import reactor +import constants as cs + tp_name_prefix = 'org.freedesktop.Telepathy' tp_path_prefix = '/org/freedesktop/Telepathy' -class TryNextHandler(Exception): - pass - -def lazy(func): - def handler(event, data): - if func(event, data): - return True - else: - raise TryNextHandler() - handler.__name__ = func.__name__ - return handler - -def match(type, **kw): - def decorate(func): - def handler(event, data, *extra, **extra_kw): - if event.type != type: - return False - - for key, value in kw.iteritems(): - if not hasattr(event, key): - return False - - if getattr(event, key) != value: - return False - - return func(event, data, *extra, **extra_kw) - - handler.__name__ = func.__name__ - return handler - - return decorate - class Event: def __init__(self, type, **kw): self.__dict__.update(kw) @@ -68,118 +38,24 @@ def format_event(event): return ret -class EventTest: - """Somewhat odd event dispatcher for asynchronous tests. - - Callbacks are kept in a queue. Incoming events are passed to the first - callback. If the callback returns True, the callback is removed. If the - callback raises AssertionError, the test fails. If there are no more - callbacks, the test passes. The reactor is stopped when the test passes. - """ - - def __init__(self): - self.queue = [] - self.data = {'test': self} - self.timeout_delayed_call = reactor.callLater(5, self.timeout_cb) - #self.verbose = True - self.verbose = False - # ugh - self.stopping = False - - def timeout_cb(self): - print 'timed out waiting for events' - print self.queue[0] - self.fail() - - def fail(self): - # ugh; better way to stop the reactor and exit(1)? - import os - os._exit(1) - - def expect(self, f): - self.queue.append(f) - - def log(self, s): - if self.verbose: - print s - - def try_stop(self): - if self.stopping: - return True - - if not self.queue: - self.log('no handlers left; stopping') - self.stopping = True - reactor.stop() - return True - - return False - - def call_handlers(self, event): - self.log('trying %r' % self.queue[0]) - handler = self.queue.pop(0) - - try: - ret = handler(event, self.data) - if not ret: - self.queue.insert(0, handler) - except TryNextHandler, e: - if self.queue: - ret = self.call_handlers(event) - else: - ret = False - self.queue.insert(0, handler) - - return ret - - def handle_event(self, event): - if self.try_stop(): - return - - self.log('got event:') - self.log('- type: %s' % event.type) - map(self.log, format_event(event)) - - try: - ret = self.call_handlers(event) - except SystemExit, e: - if e.code: - print "Unsuccessful exit:", e - self.fail() - else: - self.queue[:] = [] - ret = True - except AssertionError, e: - print 'test failed:' - traceback.print_exc() - self.fail() - except (Exception, KeyboardInterrupt), e: - print 'error in handler:' - traceback.print_exc() - self.fail() - - if ret not in (True, False): - print ("warning: %s() returned something other than True or False" - % self.queue[0].__name__) - - if ret: - self.timeout_delayed_call.reset(5) - self.log('event handled') - else: - self.log('event not handled') - - self.log('') - self.try_stop() - class EventPattern: def __init__(self, type, **properties): self.type = type - self.predicate = lambda x: True + self.predicate = None if 'predicate' in properties: self.predicate = properties['predicate'] del properties['predicate'] self.properties = properties + def __repr__(self): + properties = dict(self.properties) + + if self.predicate is not None: + properties['predicate'] = self.predicate + + return '%s(%r, **%r)' % ( + self.__class__.__name__, self.type, properties) + def match(self, event): if event.type != self.type: return False @@ -191,7 +67,7 @@ class EventPattern: except AttributeError: return False - if self.predicate(event): + if self.predicate is None or self.predicate(event): return True return False @@ -208,7 +84,7 @@ class BaseEventQueue: def __init__(self, timeout=None): self.verbose = False - self.past_events = [] + self.forbidden_events = set() if timeout is None: self.timeout = 5 @@ -219,36 +95,50 @@ class BaseEventQueue: if self.verbose: print s - def flush_past_events(self): - self.past_events = [] - - def expect_racy(self, type, **kw): - pattern = EventPattern(type, **kw) + def log_event(self, event): + if self.verbose: + self.log('got event:') - for event in self.past_events: - if pattern.match(event): - self.log('past event handled') + if self.verbose: map(self.log, format_event(event)) - self.log('') - self.past_events.remove(event) - return event - return self.expect(type, **kw) + def forbid_events(self, patterns): + """ + Add patterns (an iterable of EventPattern) to the set of forbidden + events. If a forbidden event occurs during an expect or expect_many, + the test will fail. + """ + self.forbidden_events.update(set(patterns)) + + def unforbid_events(self, patterns): + """ + Remove 'patterns' (an iterable of EventPattern) from the set of + forbidden events. These must be the same EventPattern pointers that + were passed to forbid_events. + """ + self.forbidden_events.difference_update(set(patterns)) + + def _check_forbidden(self, event): + for e in self.forbidden_events: + if e.match(event): + print "forbidden event occurred:" + for x in format_event(event): + print x + assert False def expect(self, type, **kw): pattern = EventPattern(type, **kw) while True: event = self.wait() - self.log('got event:') - map(self.log, format_event(event)) + self.log_event(event) + self._check_forbidden(event) if pattern.match(event): self.log('handled') self.log('') return event - self.past_events.append(event) self.log('not handled') self.log('') @@ -256,18 +146,25 @@ class BaseEventQueue: ret = [None] * len(patterns) while None in ret: - event = self.wait() - self.log('got event:') - map(self.log, format_event(event)) + try: + event = self.wait() + except TimeoutError: + self.log('timeout') + self.log('still expecting:') + for i, pattern in enumerate(patterns): + if ret[i] is None: + self.log(' - %r' % pattern) + raise + self.log_event(event) + self._check_forbidden(event) for i, pattern in enumerate(patterns): - if pattern.match(event): + if ret[i] is None and pattern.match(event): self.log('handled') self.log('') ret[i] = event break else: - self.past_events.append(event) self.log('not handled') self.log('') @@ -277,8 +174,7 @@ class BaseEventQueue: pattern = EventPattern(type, **kw) event = self.wait() - self.log('got event:') - map(self.log, format_event(event)) + self.log_event(event) if pattern.match(event): self.log('handled') @@ -343,6 +239,16 @@ class EventQueueTest(unittest.TestCase): assert bar.type == 'bar' assert foo.type == 'foo' + def test_expect_many2(self): + # Test that events are only matched against patterns that haven't yet + # been matched. This tests a regression. + queue = TestEventQueue([Event('foo', x=1), Event('foo', x=2)]) + foo1, foo2 = queue.expect_many( + EventPattern('foo'), + EventPattern('foo')) + assert foo1.type == 'foo' and foo1.x == 1 + assert foo2.type == 'foo' and foo2.x == 2 + def test_timeout(self): queue = TestEventQueue([]) self.assertRaises(TimeoutError, queue.expect, 'foo') @@ -369,7 +275,10 @@ def unwrap(x): if isinstance(x, dict): return dict([(unwrap(k), unwrap(v)) for k, v in x.iteritems()]) - for t in [unicode, str, long, int, float, bool]: + if isinstance(x, dbus.Boolean): + return bool(x) + + for t in [unicode, str, long, int, float]: if isinstance(x, t): return t(x) @@ -384,7 +293,8 @@ def call_async(test, proxy, method, *args, **kw): value=unwrap(ret))) def error_func(err): - test.handle_event(Event('dbus-error', method=method, error=err)) + test.handle_event(Event('dbus-error', method=method, error=err, + name=err.get_dbus_name(), message=str(err))) method_proxy = getattr(proxy, method) kw.update({'reply_handler': reply_func, 'error_handler': error_func}) @@ -392,14 +302,20 @@ def call_async(test, proxy, method, *args, **kw): def sync_dbus(bus, q, conn): # Dummy D-Bus method call - call_async(q, conn, "InspectHandles", 1, []) - - event = q.expect('dbus-return', method='InspectHandles') + # This won't do the right thing unless the proxy has a unique name. + assert conn.object.bus_name.startswith(':') + root_object = bus.get_object(conn.object.bus_name, '/') + call_async( + q, dbus.Interface(root_object, 'org.freedesktop.DBus.Peer'), 'Ping') + q.expect('dbus-return', method='Ping') class ProxyWrapper: def __init__(self, object, default, others): self.object = object self.default_interface = dbus.Interface(object, default) + self.Properties = dbus.Interface(object, dbus.PROPERTIES_IFACE) + self.TpProperties = \ + dbus.Interface(object, tp_name_prefix + '.Properties') self.interfaces = dict([ (name, dbus.Interface(object, iface)) for name, iface in others.iteritems()]) @@ -413,6 +329,33 @@ class ProxyWrapper: return getattr(self.default_interface, name) +def wrap_connection(conn): + return ProxyWrapper(conn, tp_name_prefix + '.Connection', + dict([ + (name, tp_name_prefix + '.Connection.Interface.' + name) + for name in ['Aliasing', 'Avatars', 'Capabilities', 'Contacts', + 'Presence', 'SimplePresence', 'Requests']] + + [('Peer', 'org.freedesktop.DBus.Peer'), + ('ContactCapabilities', cs.CONN_IFACE_CONTACT_CAPS), + ('ContactInfo', cs.CONN_IFACE_CONTACT_INFO), + ('Location', cs.CONN_IFACE_LOCATION), + ('Future', tp_name_prefix + '.Connection.FUTURE'), + ('MailNotification', cs.CONN_IFACE_MAIL_NOTIFICATION), + ])) + +def wrap_channel(chan, type_, extra=None): + interfaces = { + type_: tp_name_prefix + '.Channel.Type.' + type_, + 'Group': tp_name_prefix + '.Channel.Interface.Group', + } + + if extra: + interfaces.update(dict([ + (name, tp_name_prefix + '.Channel.Interface.' + name) + for name in extra])) + + return ProxyWrapper(chan, tp_name_prefix + '.Channel', interfaces) + def make_connection(bus, event_func, name, proto, params): cm = bus.get_object( tp_name_prefix + '.ConnectionManager.%s' % name, @@ -421,29 +364,7 @@ def make_connection(bus, event_func, name, proto, params): connection_name, connection_path = cm_iface.RequestConnection( proto, params) - conn = bus.get_object(connection_name, connection_path) - conn = ProxyWrapper(conn, tp_name_prefix + '.Connection', - dict([ - (name, tp_name_prefix + '.Connection.Interface.' + name) - for name in ['Aliasing', 'Avatars', 'Capabilities', 'Contacts', - 'Presence', 'SimplePresence', 'Requests']] + - [('Peer', 'org.freedesktop.DBus.Peer')])) - - bus.add_signal_receiver( - lambda *args, **kw: - event_func( - Event('dbus-signal', - path=unwrap(kw['path'])[len(tp_path_prefix):], - signal=kw['member'], args=map(unwrap, args), - interface=kw['interface'])), - None, # signal name - None, # interface - cm._named_service, - path_keyword='path', - member_keyword='member', - interface_keyword='interface', - byte_arrays=True - ) + conn = wrap_connection(bus.get_object(connection_name, connection_path)) return conn @@ -453,20 +374,12 @@ def make_channel_proxy(conn, path, iface): chan = dbus.Interface(chan, tp_name_prefix + '.' + iface) return chan -def load_event_handlers(): - path, _, _, _ = traceback.extract_stack()[0] - import compiler - import __main__ - ast = compiler.parseFile(path) - return [ - getattr(__main__, node.name) - for node in ast.node.asList() - if node.__class__ == compiler.ast.Function and - node.name.startswith('expect_')] - +# block_reading can be used if the test want to choose when we start to read +# data from the socket. class EventProtocol(Protocol): - def __init__(self, queue=None): + def __init__(self, queue=None, block_reading=False): self.queue = queue + self.block_reading = block_reading def dataReceived(self, data): if self.queue is not None: @@ -476,12 +389,24 @@ class EventProtocol(Protocol): def sendData(self, data): self.transport.write(data) + def connectionMade(self): + if self.block_reading: + self.transport.stopReading() + + def connectionLost(self, reason=None): + if self.queue is not None: + self.queue.handle_event(Event('socket-disconnected', protocol=self)) + class EventProtocolFactory(Factory): - def __init__(self, queue): + def __init__(self, queue, block_reading=False): self.queue = queue + self.block_reading = block_reading + + def _create_protocol(self): + return EventProtocol(self.queue, self.block_reading) def buildProtocol(self, addr): - proto = EventProtocol(self.queue) + proto = self._create_protocol() self.queue.handle_event(Event('socket-connected', protocol=proto)) return proto @@ -500,6 +425,72 @@ def watch_tube_signals(q, tube): path_keyword='path', member_keyword='member', byte_arrays=True) +def pretty(x): + return pprint.pformat(unwrap(x)) + +def assertEquals(expected, value): + if expected != value: + raise AssertionError( + "expected:\n%s\ngot:\n%s" % (pretty(expected), pretty(value))) + +def assertNotEquals(expected, value): + if expected == value: + raise AssertionError( + "expected something other than:\n%s" % pretty(value)) + +def assertContains(element, value): + if element not in value: + raise AssertionError( + "expected:\n%s\nin:\n%s" % (pretty(element), pretty(value))) + +def assertDoesNotContain(element, value): + if element in value: + raise AssertionError( + "expected:\n%s\nnot in:\n%s" % (pretty(element), pretty(value))) + +def assertLength(length, value): + if len(value) != length: + raise AssertionError("expected: length %d, got length %d:\n%s" % ( + length, len(value), pretty(value))) + +def assertFlagsSet(flags, value): + masked = value & flags + if masked != flags: + raise AssertionError( + "expected flags %u, of which only %u are set in %u" % ( + flags, masked, value)) + +def assertFlagsUnset(flags, value): + masked = value & flags + if masked != 0: + raise AssertionError( + "expected none of flags %u, but %u are set in %u" % ( + flags, masked, value)) + +def install_colourer(): + def red(s): + return '\x1b[31m%s\x1b[0m' % s + + def green(s): + return '\x1b[32m%s\x1b[0m' % s + + patterns = { + 'handled': green, + 'not handled': red, + } + + class Colourer: + def __init__(self, fh, patterns): + self.fh = fh + self.patterns = patterns + + def write(self, s): + f = self.patterns.get(s, lambda x: x) + self.fh.write(f(s)) + + sys.stdout = Colourer(sys.stdout, patterns) + return sys.stdout + if __name__ == '__main__': unittest.main() diff --git a/tests/twisted/text/destroy.py b/tests/twisted/text/destroy.py index fdf4f34..cd3805b 100644 --- a/tests/twisted/text/destroy.py +++ b/tests/twisted/text/destroy.py @@ -8,7 +8,7 @@ import dbus from twisted.words.xish import domish from hazetest import exec_test -from servicetest import call_async, EventPattern, tp_path_prefix +from servicetest import call_async, EventPattern, assertEquals def test(q, bus, conn, stream): conn.Connect() @@ -122,8 +122,7 @@ def test(q, bus, conn, stream): call_async(q, destroyable_iface, 'Destroy') event = q.expect('dbus-signal', signal='Closed') - assert tp_path_prefix + event.path == text_chan.object_path,\ - (tp_path_prefix + event.path, text_chan.object_path) + assertEquals(text_chan.object_path, event.path) event = q.expect('dbus-return', method='Destroy') diff --git a/tests/twisted/text/respawn.py b/tests/twisted/text/respawn.py index 966c417..f716676 100644 --- a/tests/twisted/text/respawn.py +++ b/tests/twisted/text/respawn.py @@ -7,7 +7,7 @@ import dbus from twisted.words.xish import domish from hazetest import exec_test -from servicetest import call_async, EventPattern, tp_path_prefix +from servicetest import call_async, EventPattern, assertEquals def test(q, bus, conn, stream): conn.Connect() @@ -122,10 +122,8 @@ def test(q, bus, conn, stream): EventPattern('dbus-signal', signal='Closed'), EventPattern('dbus-signal', signal='ChannelClosed'), ) - assert tp_path_prefix + old.path == text_chan.object_path,\ - (tp_path_prefix + old.path, text_chan.object_path) - assert new.args[0] == text_chan.object_path,\ - (new.args[0], text_chan.object_path) + assertEquals(text_chan.object_path, old.path) + assertEquals(text_chan.object_path, new.args[0]) event = q.expect('dbus-signal', signal='NewChannel') assert event.args[0] == text_chan.object_path @@ -171,8 +169,7 @@ def test(q, bus, conn, stream): call_async(q, chan_iface, 'Close') event = q.expect('dbus-signal', signal='Closed') - assert tp_path_prefix + event.path == text_chan.object_path,\ - (tp_path_prefix + event.path, text_chan.object_path) + assertEquals(text_chan.object_path, event.path) event = q.expect('dbus-return', method='Close') |