summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon McVittie <simon.mcvittie@collabora.co.uk>2010-07-01 13:21:09 +0100
committerSimon McVittie <simon.mcvittie@collabora.co.uk>2010-07-01 13:21:13 +0100
commit70eb66f5d42033fbabfe25895216345e64cda4b4 (patch)
tree4b8c6cd21f2162669e9c28a0ddda877c8c869d63
parent86333c7d2b313f59a22d0ef611fa6b4074b08bac (diff)
parent7f234add16a76d62c42c7616e11b3492017f2253 (diff)
downloadtelepathy-haze-70eb66f5d42033fbabfe25895216345e64cda4b4.tar.gz
Merge branch 'contact-list'
Reviewed-by: Will Thompson <will.thompson@collabora.co.uk>
-rw-r--r--configure.ac2
-rw-r--r--src/connection.c15
-rw-r--r--src/connection.h2
-rw-r--r--src/contact-list-channel.c74
-rwxr-xr-xtests/exec-with-log.sh1
-rw-r--r--tests/twisted/Makefile.am5
-rw-r--r--tests/twisted/constants.py37
-rw-r--r--tests/twisted/hazetest.py86
-rw-r--r--tests/twisted/ns.py39
-rw-r--r--tests/twisted/roster/groups.py209
-rw-r--r--tests/twisted/roster/initial-roster.py144
-rw-r--r--tests/twisted/roster/publish.py137
-rw-r--r--tests/twisted/roster/removed-from-rp-subscribe.py148
-rw-r--r--tests/twisted/roster/subscribe.py110
-rw-r--r--tests/twisted/servicetest.py397
-rw-r--r--tests/twisted/text/destroy.py5
-rw-r--r--tests/twisted/text/respawn.py11
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')