diff options
author | Will Thompson <will.thompson@collabora.co.uk> | 2013-04-11 19:30:32 +0100 |
---|---|---|
committer | Will Thompson <will.thompson@collabora.co.uk> | 2013-04-11 19:42:30 +0100 |
commit | 9ee8b96ad91af52936f5bee5b9a472de645c2363 (patch) | |
tree | 876b785a48c06908ae10e5b7c4f2e60b1a84fecf | |
parent | 4068d8504b2155168b82144a4d45b138e0579ef2 (diff) | |
download | telepathy-haze-9ee8b96ad91af52936f5bee5b9a472de645c2363.tar.gz |
Refresh and uncopypasta gabbletest
hazetest now overrides what it needs to, and reuses the rest of
gabbletest verbatim in a separate module. I had to add a couple of hooks
which I'll commit to Gabble.
24 files changed, 1129 insertions, 876 deletions
diff --git a/tests/twisted/avatar-requirements.py b/tests/twisted/avatar-requirements.py index 706fdc0..8cd3d77 100644 --- a/tests/twisted/avatar-requirements.py +++ b/tests/twisted/avatar-requirements.py @@ -60,4 +60,4 @@ def test(q, bus, conn, stream): #assert maxb == 8192, maxb if __name__ == '__main__': - exec_test(test) + exec_test(test, do_connect=False) diff --git a/tests/twisted/cm/protocols.py b/tests/twisted/cm/protocols.py index 2231597..4dc3f19 100644 --- a/tests/twisted/cm/protocols.py +++ b/tests/twisted/cm/protocols.py @@ -181,11 +181,6 @@ def test(q, bus, conn, stream): 'login': r'WORKGROUP\Bill', 'password': 'letmein'})) - conn.Connect() - q.expect('dbus-signal', signal='StatusChanged', args=[1, 1]) - conn.Disconnect() - q.expect('dbus-signal', signal='StatusChanged', args=[2, 1]) - if __name__ == '__main__': exec_test(test) diff --git a/tests/twisted/connect/fail.py b/tests/twisted/connect/fail.py index db24cdb..a45b2b2 100644 --- a/tests/twisted/connect/fail.py +++ b/tests/twisted/connect/fail.py @@ -24,5 +24,5 @@ def test(q, bus, conn, stream): args=[cs.CONN_STATUS_DISCONNECTED, cs.CSR_NETWORK_ERROR]) if __name__ == '__main__': - exec_test(test, {'port': dbus.UInt32(14243)}) + exec_test(test, {'port': dbus.UInt32(14243)}, do_connect=False) diff --git a/tests/twisted/connect/success.py b/tests/twisted/connect/success.py index d9cdb4e..c16f049 100644 --- a/tests/twisted/connect/success.py +++ b/tests/twisted/connect/success.py @@ -4,23 +4,20 @@ Test connecting to a server. """ from hazetest import exec_test +import constants as cs def test(q, bus, conn, stream): conn.Connect() - q.expect('dbus-signal', signal='StatusChanged', args=[1, 1]) + q.expect('dbus-signal', signal='StatusChanged', args=[cs.CONN_STATUS_CONNECTING, cs.CSR_REQUESTED]) q.expect('stream-authenticated') # FIXME: unlike Gabble, Haze does not signal a presence update to # available during connect - #q.expect('dbus-signal', signal='PresenceUpdate', - # args=[{1L: (0L, {u'available': {}})}]) + #q.expect('dbus-signal', signal='PresencesChanged', + # args=[{1L: (cs.PRESENCE_AVAILABLE, 'available', '')}]) - q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) - - conn.Disconnect() - q.expect('dbus-signal', signal='StatusChanged', args=[2, 1]) - return True + q.expect('dbus-signal', signal='StatusChanged', args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) if __name__ == '__main__': - exec_test(test) + exec_test(test, do_connect=False) diff --git a/tests/twisted/connect/twice-to-same-account.py b/tests/twisted/connect/twice-to-same-account.py index fb2c25e..8fe3db2 100644 --- a/tests/twisted/connect/twice-to-same-account.py +++ b/tests/twisted/connect/twice-to-same-account.py @@ -13,19 +13,6 @@ from servicetest import ( import constants as cs def test(q, bus, conn, stream): - conn.Connect() - q.expect_many( - EventPattern('dbus-signal', signal='StatusChanged', args=[1, 1]), - EventPattern('stream-authenticated'), - ) - - # FIXME: unlike Gabble, Haze does not signal a presence update to - # available during connect - #q.expect('dbus-signal', signal='PresenceUpdate', - # args=[{1L: (0L, {u'available': {}})}]) - - q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) - haze = bus.get_object( tp_name_prefix + '.ConnectionManager.haze', tp_path_prefix + '/ConnectionManager/haze') diff --git a/tests/twisted/constants.py b/tests/twisted/constants.py index 1d2edf8..d541c75 100644 --- a/tests/twisted/constants.py +++ b/tests/twisted/constants.py @@ -24,10 +24,14 @@ 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_IFACE_SASL_AUTH = CHANNEL + ".Interface.SASLAuthentication" CHANNEL_IFACE_CONFERENCE = CHANNEL + '.Interface.Conference' +CHANNEL_IFACE_ROOM = CHANNEL + '.Interface.Room2' +CHANNEL_IFACE_ROOM_CONFIG = CHANNEL + '.Interface.RoomConfig1' +CHANNEL_IFACE_SUBJECT = CHANNEL + '.Interface.Subject2' +CHANNEL_IFACE_FILE_TRANSFER_METADATA = CHANNEL + '.Interface.FileTransfer.Metadata' -CHANNEL_TYPE_CALL = CHANNEL + ".Type.Call.DRAFT" +CHANNEL_TYPE_CALL = CHANNEL + ".Type.Call1" CHANNEL_TYPE_CONTACT_LIST = CHANNEL + ".Type.ContactList" CHANNEL_TYPE_CONTACT_SEARCH = CHANNEL + ".Type.ContactSearch" CHANNEL_TYPE_TEXT = CHANNEL + ".Type.Text" @@ -38,7 +42,7 @@ 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" + CHANNEL + ".Type.ServerAuthentication" CHANNEL_TYPE_SERVER_TLS_CONNECTION = \ CHANNEL + ".Type.ServerTLSConnection" @@ -61,45 +65,103 @@ INITIAL_VIDEO = CHANNEL_TYPE_STREAMED_MEDIA + '.InitialVideo' IMMUTABLE_STREAMS = CHANNEL_TYPE_STREAMED_MEDIA + '.ImmutableStreams' CALL_INITIAL_AUDIO = CHANNEL_TYPE_CALL + '.InitialAudio' +CALL_INITIAL_AUDIO_NAME = CHANNEL_TYPE_CALL + '.InitialAudioName' CALL_INITIAL_VIDEO = CHANNEL_TYPE_CALL + '.InitialVideo' +CALL_INITIAL_VIDEO_NAME = CHANNEL_TYPE_CALL + '.InitialVideoName' CALL_MUTABLE_CONTENTS = CHANNEL_TYPE_CALL + '.MutableContents' -CALL_CONTENT = 'org.freedesktop.Telepathy.Call.Content.DRAFT' +CALL_CONTENT = 'org.freedesktop.Telepathy.Call1.Content' CALL_CONTENT_IFACE_MEDIA = \ - 'org.freedesktop.Telepathy.Call.Content.Interface.Media.DRAFT' + 'org.freedesktop.Telepathy.Call1.Content.Interface.Media' +CALL_CONTENT_IFACE_DTMF = \ + 'org.freedesktop.Telepathy.Call1.Content.Interface.DTMF' -CALL_CONTENT_CODECOFFER = \ - 'org.freedesktop.Telepathy.Call.Content.CodecOffer.DRAFT' +CALL_CONTENT_MEDIADESCRIPTION = \ + 'org.freedesktop.Telepathy.Call1.Content.MediaDescription' -CALL_STREAM = 'org.freedesktop.Telepathy.Call.Stream.DRAFT' +CALL_STREAM = 'org.freedesktop.Telepathy.Call1.Stream' CALL_STREAM_IFACE_MEDIA = \ - 'org.freedesktop.Telepathy.Call.Stream.Interface.Media.DRAFT' + 'org.freedesktop.Telepathy.Call1.Stream.Interface.Media' -CALL_STREAM_ENDPOINT = 'org.freedesktop.Telepathy.Call.Stream.Endpoint.DRAFT' +CALL_STREAM_ENDPOINT = 'org.freedesktop.Telepathy.Call1.Stream.Endpoint' CALL_MEDIA_TYPE_AUDIO = 0 CALL_MEDIA_TYPE_VIDEO = 1 -CALL_STREAM_TRANSPORT_RAW_UDP = 0 -CALL_STREAM_TRANSPORT_ICE = 1 -CALL_STREAM_TRANSPORT_GOOGLE = 2 +CALL_CONTENT_PACKETIZATION_RTP = 0 +CALL_CONTENT_PACKETIZATION_RAW = 1 +CALL_CONTENT_PACKETIZATION_MSN_WEBCAM = 2 -CALL_STATE_UNKNOWN = 0 +CALL_STREAM_TRANSPORT_UNKNOWN = 0 +CALL_STREAM_TRANSPORT_RAW_UDP = 1 +CALL_STREAM_TRANSPORT_ICE = 2 +CALL_STREAM_TRANSPORT_GTALK_P2P = 3 +CALL_STREAM_TRANSPORT_WLM_2009 = 4 +CALL_STREAM_TRANSPORT_SHM = 5 +CALL_STREAM_TRANSPORT_MULTICAST = 6 + +#for streamed media +CALL_STATE_RINGING = 1 +CALL_STATE_HELD = 4 + +CALL_STATE_UNKNOWN = 0, CALL_STATE_PENDING_INITIATOR = 1 -CALL_STATE_PENDING_RECEIVER = 2 -CALL_STATE_ACCEPTED = 3 -CALL_STATE_ENDED = 4 +CALL_STATE_INITIALISING = 2 +CALL_STATE_INITIALISED = 3 +CALL_STATE_ACCEPTED = 4 +CALL_STATE_ACTIVE = 5 +CALL_STATE_ENDED = 6 + +CALL_FLAG_LOCALLY_HELD = 1 +CALL_FLAG_LOCALLY_RINGING = 2 +CALL_FLAG_LOCALLY_QUEUED = 4 +CALL_FLAG_FORWARDED = 8 +CALL_FLAG_CLEARING = 16 CALL_MEMBER_FLAG_RINGING = 1 CALL_MEMBER_FLAG_HELD = 2 CALL_DISPOSITION_NONE = 0 -CALL_DISPOSITION_EARLY_MEDIA = 1 -CALL_DISPOSITION_INITIAL = 2 +CALL_DISPOSITION_INITIAL = 1 CALL_SENDING_STATE_NONE = 0 CALL_SENDING_STATE_PENDING_SEND = 1 CALL_SENDING_STATE_SENDING = 2 +CALL_SENDING_STATE_PENDING_STOP_SENDING = 3 + +CALL_STREAM_FLOW_STATE_STOPPED = 0 +CALL_STREAM_FLOW_STATE_PENDING_START = 1 +CALL_STREAM_FLOW_STATE_PENDING_STOP = 2 +CALL_STREAM_FLOW_STATE_STARTED = 3 + +CALL_STREAM_ENDPOINT_STATE_CONNECTING = 0 +CALL_STREAM_ENDPOINT_STATE_PROVISIONALLY_CONNECTED = 1 +CALL_STREAM_ENDPOINT_STATE_FULLY_CONNECTED = 2 +CALL_STREAM_ENDPOINT_STATE_EXHAUSTED_CANDIDATES = 3 +CALL_STREAM_ENDPOINT_STATE_FAILED = 4 + +CALL_STREAM_CANDIDATE_TYPE_HOST = 1 +CALL_STREAM_CANDIDATE_TYPE_SERVER_REFLEXIVE = 2 +CALL_STREAM_CANDIDATE_TYPE_RELAY = 4 + +CALL_STATE_CHANGE_REASON_UNKNOWN = 0 +CALL_STATE_CHANGE_REASON_PROGRESS_MADE = 1 +CALL_STATE_CHANGE_REASON_USER_REQUESTED = 2 +CALL_STATE_CHANGE_REASON_FORWARDED = 3 +CALL_STATE_CHANGE_REASON_REJECTED = 4 +CALL_STATE_CHANGE_REASON_NO_ANSWER = 5 +CALL_STATE_CHANGE_REASON_INVALID_CONTACT = 6 +CALL_STATE_CHANGE_REASON_PERMISSION_DENIED = 7 +CALL_STATE_CHANGE_REASON_BUSY = 8 +CALL_STATE_CHANGE_REASON_INTERNAL_ERROR = 9 +CALL_STATE_CHANGE_REASON_SERVICE_ERROR = 10 +CALL_STATE_CHANGE_REASON_NETWORK_ERROR = 11 +CALL_STATE_CHANGE_REASON_MEDIA_ERROR = 12 +CALL_STATE_CHANGE_REASON_CONNECTIVITY_ERROR = 13 + +CALL_STREAM_COMPONENT_UNKNOWN = 0 +CALL_STREAM_COMPONENT_DATA = 1 +CALL_STREAM_COMPONENT_CONTROL = 2 SUBSCRIPTION_STATE_UNKNOWN = 0 SUBSCRIPTION_STATE_NO = 1 @@ -129,6 +191,8 @@ CONN_IFACE_CONTACT_LIST = CONN + '.Interface.ContactList' CONN_IFACE_CONTACT_GROUPS = CONN + '.Interface.ContactGroups' CONN_IFACE_CLIENT_TYPES = CONN + '.Interface.ClientTypes' CONN_IFACE_POWER_SAVING = CONN + '.Interface.PowerSaving' +CONN_IFACE_CONTACT_BLOCKING = CONN + '.Interface.ContactBlocking' +CONN_IFACE_ADDRESSING = CONN + '.Interface.Addressing1' ATTR_CONTACT_CAPABILITIES = CONN_IFACE_CONTACT_CAPS + '/capabilities' @@ -155,6 +219,9 @@ NOT_YET = ERROR + '.NotYet' INVALID_HANDLE = ERROR + '.InvalidHandle' CERT_UNTRUSTED = ERROR + '.Cert.Untrusted' SERVICE_BUSY = ERROR + '.ServiceBusy' +SERVICE_CONFUSED = ERROR + '.ServiceConfused' + +BANNED = ERROR + '.Channel.Banned' UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod' @@ -188,6 +255,13 @@ TUBE_CHANNEL_STATE_NOT_OFFERED = 3 MEDIA_STREAM_TYPE_AUDIO = 0 MEDIA_STREAM_TYPE_VIDEO = 1 +MEDIA_STREAM_BASE_PROTO_UDP = 0 +MEDIA_STREAM_BASE_PROTO_TCP = 1 + +MEDIA_STREAM_TRANSPORT_TYPE_LOCAL = 0 +MEDIA_STREAM_TRANSPORT_TYPE_DERIVED = 1 +MEDIA_STREAM_TRANSPORT_TYPE_RELAY = 2 + SOCKET_ADDRESS_TYPE_UNIX = 0 SOCKET_ADDRESS_TYPE_ABSTRACT_UNIX = 1 SOCKET_ADDRESS_TYPE_IPV4 = 2 @@ -257,6 +331,9 @@ 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' +FT_URI = CHANNEL_TYPE_FILE_TRANSFER + '.URI' +FT_SERVICE_NAME = CHANNEL_IFACE_FILE_TRANSFER_METADATA + '.ServiceName' +FT_METADATA = CHANNEL_IFACE_FILE_TRANSFER_METADATA + '.Metadata' GF_CAN_ADD = 1 GF_CAN_REMOVE = 2 @@ -294,11 +371,6 @@ HSR_NONE = 0 HSR_REQUESTED = 1 HSR_RESOURCE_NOT_AVAILABLE = 2 -CALL_STATE_RINGING = 1 -CALL_STATE_QUEUED = 2 -CALL_STATE_HELD = 4 -CALL_STATE_FORWARDED = 8 - CONN_STATUS_CONNECTED = 0 CONN_STATUS_CONNECTING = 1 CONN_STATUS_DISCONNECTED = 2 @@ -348,11 +420,8 @@ 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 +CONTACT_INFO_FIELD_FLAG_PARAMETERS_EXACT = 1 +CONTACT_INFO_FIELD_FLAG_OVERWRITTEN_BY_NICKNAME = 2 # Channel_Interface_SaslAuthentication SASL_STATUS_NOT_STARTED = 0 @@ -367,12 +436,20 @@ 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" +SASL_STATUS = CHANNEL_IFACE_SASL_AUTH + ".SASLStatus" +SASL_ERROR = CHANNEL_IFACE_SASL_AUTH + ".SASLError" +SASL_ERROR_DETAILS = CHANNEL_IFACE_SASL_AUTH + ".SASLErrorDetails" +SASL_CONTEXT = CHANNEL_IFACE_SASL_AUTH + ".SASLContext" +SASL_AUTHORIZATION_IDENTITY = CHANNEL_IFACE_SASL_AUTH + ".AuthorizationIdentity" +SASL_DEFAULT_REALM = CHANNEL_IFACE_SASL_AUTH + ".DefaultRealm" +SASL_DEFAULT_USERNAME = CHANNEL_IFACE_SASL_AUTH + ".DefaultUsername" # Channel_Type_ServerTLSConnection TLS_CERT_PATH = CHANNEL_TYPE_SERVER_TLS_CONNECTION + ".ServerCertificate" TLS_HOSTNAME = CHANNEL_TYPE_SERVER_TLS_CONNECTION + ".Hostname" +TLS_REFERENCE_IDENTITIES = \ + CHANNEL_TYPE_SERVER_TLS_CONNECTION + ".ReferenceIdentities" # Connection.Interface.Location @@ -386,7 +463,24 @@ MT_NOTICE = 2 MT_AUTO_REPLY = 3 MT_DELIVERY_REPORT = 4 +class MessageFlag(object): + TRUNCATED = 1 + NON_TEXT_CONTENT = 2 + SCROLLBACK = 4 + RESCUED = 8 + +class SendError(object): + UNKNOWN = 0 + OFFLINE = 1 + INVALID_CONTACT = 2 + PERMISSION_DENIED = 3 + TOO_LONG = 4 + NOT_IMPLEMENTED = 5 + PROTOCOL = 'org.freedesktop.Telepathy.Protocol' +PROTOCOL_IFACE_PRESENCES = PROTOCOL + '.Interface.Presence' +PROTOCOL_IFACE_ADDRESSING = PROTOCOL + '.Interface.Addressing' + PARAM_REQUIRED = 1 PARAM_REGISTER = 2 PARAM_HAS_DEFAULT = 4 @@ -417,3 +511,31 @@ DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_FAILURES = 1 DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_SUCCESSES = 2 DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_READ = 4 DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_DELETED = 8 + +DELIVERY_STATUS_UNKNOWN = 0 +DELIVERY_STATUS_DELIVERED = 1 +DELIVERY_STATUS_TEMPORARILY_FAILED = 2 +DELIVERY_STATUS_PERMANENTLY_FAILED = 3 +DELIVERY_STATUS_ACCEPTED = 4 +DELIVERY_STATUS_READ = 5 +DELIVERY_STATUS_DELETED = 6 + +MEDIA_STREAM_ERROR_UNKNOWN = 0 +MEDIA_STREAM_ERROR_EOS = 1 +MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED = 2 +MEDIA_STREAM_ERROR_CONNECTION_FAILED = 3 +MEDIA_STREAM_ERROR_NETWORK_ERROR = 4 +MEDIA_STREAM_ERROR_NO_CODECS = 5 +MEDIA_STREAM_ERROR_INVALID_CM_BEHAVIOR = 6 +MEDIA_STREAM_ERROR_MEDIA_ERROR = 7 + +PASSWORD_FLAG_PROVIDE = 8 + +# Channel.Interface.Room +ROOM_NAME = CHANNEL_IFACE_ROOM + '.RoomName' +ROOM_SERVER = CHANNEL_IFACE_ROOM + '.Server' + +# Channel.Interface.Subject +SUBJECT = CHANNEL_IFACE_ROOM + '.Subject' +SUBJECT_PRESENT = 1 +SUBJECT_CAN_SET = 2 diff --git a/tests/twisted/gabbletest.py b/tests/twisted/gabbletest.py new file mode 100644 index 0000000..82db618 --- /dev/null +++ b/tests/twisted/gabbletest.py @@ -0,0 +1,856 @@ + +""" +Infrastructure code for testing Gabble by pretending to be a Jabber server. +""" + +import base64 +import os +import hashlib +import sys +import random +import re +import traceback + +import ns +import constants as cs +import servicetest +from servicetest import ( + assertEquals, assertLength, assertContains, wrap_channel, + EventPattern, call_async, unwrap, Event) +import twisted +from twisted.words.xish import domish, xpath +from twisted.words.protocols.jabber.client import IQ +from twisted.words.protocols.jabber import xmlstream +from twisted.internet import reactor, ssl + +import dbus + +def make_result_iq(stream, iq, add_query_node=True): + result = IQ(stream, "result") + result["id"] = iq["id"] + to = iq.getAttribute('to') + if to is not None: + result["from"] = to + query = iq.firstChildElement() + + if query and add_query_node: + result.addElement((query.uri, query.name)) + + return result + +def acknowledge_iq(stream, iq): + stream.send(make_result_iq(stream, iq)) + +def send_error_reply(stream, iq, error_stanza=None): + result = IQ(stream, "error") + result["id"] = iq["id"] + query = iq.firstChildElement() + to = iq.getAttribute('to') + if to is not None: + result["from"] = to + + if query: + result.addElement((query.uri, query.name)) + + if error_stanza: + result.addChild(error_stanza) + + stream.send(result) + +def request_muc_handle(q, conn, stream, muc_jid): + servicetest.call_async(q, conn, 'RequestHandles', 2, [muc_jid]) + event = q.expect('dbus-return', method='RequestHandles') + return event.value[0][0] + +def make_muc_presence(affiliation, role, muc_jid, alias, jid=None, photo=None): + presence = domish.Element((None, 'presence')) + presence['from'] = '%s/%s' % (muc_jid, alias) + x = presence.addElement((ns.MUC_USER, 'x')) + item = x.addElement('item') + item['affiliation'] = affiliation + item['role'] = role + if jid is not None: + item['jid'] = jid + + if photo is not None: + presence.addChild( + elem(ns.VCARD_TEMP_UPDATE, 'x')( + elem('photo')(unicode(photo)) + )) + + return presence + +def sync_stream(q, stream): + """Used to ensure that Gabble has processed all stanzas sent to it.""" + + iq = IQ(stream, "get") + id = iq['id'] + iq.addElement(('http://jabber.org/protocol/disco#info', 'query')) + stream.send(iq) + q.expect('stream-iq', query_ns='http://jabber.org/protocol/disco#info', + predicate=(lambda event: + event.stanza['id'] == id and event.iq_type == 'result')) + +class GabbleAuthenticator(xmlstream.Authenticator): + def __init__(self, username, password, resource=None): + self.username = username + self.password = password + self.resource = resource + self.bare_jid = None + self.full_jid = None + self._event_func = lambda e: None + xmlstream.Authenticator.__init__(self) + + def set_event_func(self, event_func): + self._event_func = event_func + +class JabberAuthenticator(GabbleAuthenticator): + "Trivial XML stream authenticator that accepts one username/digest pair." + + # Patch in fix from http://twistedmatrix.com/trac/changeset/23418. + # This monkeypatch taken from Gadget source code + from twisted.words.xish.utility import EventDispatcher + + def _addObserver(self, onetime, event, observerfn, priority, *args, + **kwargs): + if self._dispatchDepth > 0: + self._updateQueue.append(lambda: self._addObserver(onetime, event, + observerfn, priority, *args, **kwargs)) + + return self._oldAddObserver(onetime, event, observerfn, priority, + *args, **kwargs) + + EventDispatcher._oldAddObserver = EventDispatcher._addObserver + EventDispatcher._addObserver = _addObserver + + def __init__(self, username, password, resource=None, emit_events=False): + GabbleAuthenticator.__init__(self, username, password, resource) + self.emit_events = emit_events + + def streamStarted(self, root=None): + if root: + self.xmlstream.sid = '%x' % random.randint(1, sys.maxint) + self.xmlstream.domain = root.getAttribute('to') + + self.xmlstream.sendHeader() + self.xmlstream.addOnetimeObserver( + "/iq/query[@xmlns='jabber:iq:auth']", self.initialIq) + + def initialIq(self, iq): + if self.emit_events: + self._event_func(Event('auth-initial-iq', authenticator=self, + iq=iq, id=iq["id"])) + else: + self.respondToInitialIq(iq) + + self.xmlstream.addOnetimeObserver('/iq/query/username', self.secondIq) + + def respondToInitialIq(self, iq): + result = IQ(self.xmlstream, "result") + result["id"] = iq["id"] + query = result.addElement('query') + query["xmlns"] = "jabber:iq:auth" + query.addElement('username', content='test') + query.addElement('password') + query.addElement('digest') + query.addElement('resource') + self.xmlstream.send(result) + + def secondIq(self, iq): + if self.emit_events: + self._event_func(Event('auth-second-iq', authenticator=self, + iq=iq, id=iq["id"])) + else: + self.respondToSecondIq(self, iq) + + def respondToSecondIq(self, iq): + username = xpath.queryForNodes('/iq/query/username', iq) + assert map(str, username) == [self.username] + + digest = xpath.queryForNodes('/iq/query/digest', iq) + expect = hashlib.sha1(self.xmlstream.sid + self.password).hexdigest() + assert map(str, digest) == [expect] + + resource = xpath.queryForNodes('/iq/query/resource', iq) + assertLength(1, resource) + if self.resource is not None: + assertEquals(self.resource, str(resource[0])) + + self.bare_jid = '%s@%s' % (self.username, self.xmlstream.domain) + self.full_jid = '%s/%s' % (self.bare_jid, resource) + + result = IQ(self.xmlstream, "result") + result["id"] = iq["id"] + self.xmlstream.send(result) + self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) + +class XmppAuthenticator(GabbleAuthenticator): + def __init__(self, username, password, resource=None): + GabbleAuthenticator.__init__(self, username, password, resource) + self.authenticated = False + + self._mechanisms = ['PLAIN'] + + def streamInitialize(self, root): + if root: + self.xmlstream.sid = root.getAttribute('id') + self.xmlstream.domain = root.getAttribute('to') + + if self.xmlstream.sid is None: + self.xmlstream.sid = '%x' % random.randint(1, sys.maxint) + + self.xmlstream.sendHeader() + + def streamIQ(self): + features = elem(xmlstream.NS_STREAMS, 'features')( + elem(ns.NS_XMPP_BIND, 'bind'), + elem(ns.NS_XMPP_SESSION, 'session'), + ) + self.xmlstream.send(features) + + self.xmlstream.addOnetimeObserver( + "/iq/bind[@xmlns='%s']" % ns.NS_XMPP_BIND, self.bindIq) + self.xmlstream.addOnetimeObserver( + "/iq/session[@xmlns='%s']" % ns.NS_XMPP_SESSION, self.sessionIq) + + def streamSASL(self): + features = domish.Element((xmlstream.NS_STREAMS, 'features')) + mechanisms = features.addElement((ns.NS_XMPP_SASL, 'mechanisms')) + for mechanism in self._mechanisms: + mechanisms.addElement('mechanism', content=mechanism) + self.xmlstream.send(features) + + self.xmlstream.addOnetimeObserver("/auth", self.auth) + + def streamStarted(self, root=None): + self.streamInitialize(root) + + if self.authenticated: + # Initiator authenticated itself, and has started a new stream. + self.streamIQ() + else: + self.streamSASL() + + def auth(self, auth): + assert (base64.b64decode(str(auth)) == + '\x00%s\x00%s' % (self.username, self.password)) + + success = domish.Element((ns.NS_XMPP_SASL, 'success')) + self.xmlstream.send(success) + self.xmlstream.reset() + self.authenticated = True + + def bindIq(self, iq): + resource = xpath.queryForString('/iq/bind/resource', iq) + if self.resource is not None: + assertEquals(self.resource, resource) + else: + assert resource is not None + + result = IQ(self.xmlstream, "result") + result["id"] = iq["id"] + bind = result.addElement((ns.NS_XMPP_BIND, 'bind')) + self.bare_jid = '%s@%s' % (self.username, self.xmlstream.domain) + self.full_jid = '%s/%s' % (self.bare_jid, resource) + jid = bind.addElement('jid', content=self.full_jid) + self.xmlstream.send(result) + + self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) + + def sessionIq(self, iq): + self.xmlstream.send(make_result_iq(self.xmlstream, iq)) + +class StreamEvent(servicetest.Event): + def __init__(self, type_, stanza, stream): + servicetest.Event.__init__(self, type_, stanza=stanza) + self.stream = stream + self.to = stanza.getAttribute("to") + +class IQEvent(StreamEvent): + def __init__(self, stream, iq): + StreamEvent.__init__(self, 'stream-iq', iq, stream) + self.iq_type = iq.getAttribute("type") + self.iq_id = iq.getAttribute("id") + + query = iq.firstChildElement() + + if query: + self.query = query + self.query_ns = query.uri + self.query_name = query.name + + if query.getAttribute("node"): + self.query_node = query.getAttribute("node") + else: + self.query = None + +class PresenceEvent(StreamEvent): + def __init__(self, stream, stanza): + StreamEvent.__init__(self, 'stream-presence', stanza, stream) + self.presence_type = stanza.getAttribute('type') + + statuses = xpath.queryForNodes('/presence/status', stanza) + + if statuses: + self.presence_status = str(statuses[0]) + +class MessageEvent(StreamEvent): + def __init__(self, stream, stanza): + StreamEvent.__init__(self, 'stream-message', stanza, stream) + self.message_type = stanza.getAttribute('type') + +class StreamFactory(twisted.internet.protocol.Factory): + def __init__(self, streams, jids): + self.streams = streams + self.jids = jids + self.presences = {} + self.mappings = dict(map (lambda jid, stream: (jid, stream), + jids, streams)) + + # Make a copy of the streams + self.factory_streams = list(streams) + self.factory_streams.reverse() + + # Do not add observers for single instances because it's unnecessary and + # some unit tests need to respond to the roster request, and we shouldn't + # answer it for them otherwise we break compatibility + if len(streams) > 1: + # We need to have a function here because lambda keeps a reference on + # the stream and jid and in the for loop, there is no context + def addObservers(stream, jid): + stream.addObserver('/iq', lambda x: \ + self.forward_iq(stream, jid, x)) + stream.addObserver('/presence', lambda x: \ + self.got_presence(stream, jid, x)) + + for (jid, stream) in self.mappings.items(): + addObservers(stream, jid) + + def protocol(self, *args): + return self.factory_streams.pop() + + + def got_presence (self, stream, jid, stanza): + stanza.attributes['from'] = jid + self.presences[jid] = stanza + + for dest_jid in self.presences.keys(): + # Dispatch the new presence to other clients + stanza.attributes['to'] = dest_jid + self.mappings[dest_jid].send(stanza) + + # Don't echo the presence twice + if dest_jid != jid: + # Dispatch other client's presence to this stream + presence = self.presences[dest_jid] + presence.attributes['to'] = jid + stream.send(presence) + + def lost_presence(self, stream, jid): + if self.presences.has_key(jid): + del self.presences[jid] + for dest_jid in self.presences.keys(): + presence = domish.Element(('jabber:client', 'presence')) + presence['from'] = jid + presence['to'] = dest_jid + presence['type'] = 'unavailable' + self.mappings[dest_jid].send(presence) + + def forward_iq(self, stream, jid, stanza): + stanza.attributes['from'] = jid + + query = stanza.firstChildElement() + + # Fake other accounts as being part of our roster + if query and query.uri == ns.ROSTER: + roster = make_result_iq(stream, stanza) + query = roster.firstChildElement() + for roster_jid in self.mappings.keys(): + if jid != roster_jid: + item = query.addElement('item') + item['jid'] = roster_jid + item['subscription'] = 'both' + stream.send(roster) + return + + to = stanza.getAttribute('to') + dest = None + if to is not None: + dest = self.mappings.get(to) + + if dest is not None: + dest.send(stanza) + +class BaseXmlStream(xmlstream.XmlStream): + initiating = False + namespace = 'jabber:client' + pep_support = True + disco_features = [] + handle_privacy_lists = True + + def __init__(self, event_func, authenticator): + xmlstream.XmlStream.__init__(self, authenticator) + self.event_func = event_func + self.addObserver('//iq', lambda x: event_func( + IQEvent(self, x))) + self.addObserver('//message', lambda x: event_func( + MessageEvent(self, x))) + self.addObserver('//presence', lambda x: event_func( + PresenceEvent(self, x))) + self.addObserver('//event/stream/authd', self._cb_authd) + if self.handle_privacy_lists: + self.addObserver("/iq/query[@xmlns='%s']" % ns.PRIVACY, + self._cb_priv_list) + + def connectionMade(self): + xmlstream.XmlStream.connectionMade(self) + + if 'GABBLE_NODELAY' in os.environ: + self.transport.setTcpNoDelay(True) + + def _cb_priv_list(self, iq): + send_error_reply(self, iq) + + def _cb_authd(self, _): + # called when stream is authenticated + assert self.authenticator.full_jid is not None + assert self.authenticator.bare_jid is not None + + self.addObserver( + "/iq[@to='%s']/query[@xmlns='http://jabber.org/protocol/disco#info']" % self.domain, + self._cb_disco_iq) + self.addObserver( + "/iq[@to='%s']/query[@xmlns='http://jabber.org/protocol/disco#info']" + % self.authenticator.bare_jid, + self._cb_bare_jid_disco_iq) + self.event_func(servicetest.Event('stream-authenticated')) + + def _cb_disco_iq(self, iq): + nodes = xpath.queryForNodes( + "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']", iq) + query = nodes[0] + + for feature in self.disco_features: + query.addChild(elem('feature', var=feature)) + + iq['type'] = 'result' + iq['from'] = iq['to'] + self.send(iq) + + def _cb_bare_jid_disco_iq(self, iq): + # advertise PEP support + nodes = xpath.queryForNodes( + "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']", + iq) + query = nodes[0] + identity = query.addElement('identity') + identity['category'] = 'pubsub' + identity['type'] = 'pep' + + iq['type'] = 'result' + iq['from'] = iq['to'] + self.send(iq) + + def onDocumentEnd(self): + self.event_func(servicetest.Event('stream-closed')) + # We don't chain up XmlStream.onDocumentEnd() because it will + # disconnect the TCP connection making tests as + # connect/disconnect-timeout.py not working + + def connectionLost(self, reason): + self.event_func(servicetest.Event('stream-connection-lost')) + xmlstream.XmlStream.connectionLost(self, reason) + + def send_stream_error(self, error='system-shutdown'): + # Yes, there are meant to be two different STREAMS namespaces. + go_away = \ + elem(xmlstream.NS_STREAMS, 'error')( + elem(ns.STREAMS, error) + ) + + self.send(go_away) + +class JabberXmlStream(BaseXmlStream): + version = (0, 9) + +class XmppXmlStream(BaseXmlStream): + version = (1, 0) + +class GoogleXmlStream(BaseXmlStream): + version = (1, 0) + + pep_support = False + disco_features = [ns.GOOGLE_ROSTER, + ns.GOOGLE_JINGLE_INFO, + ns.GOOGLE_MAIL_NOTIFY, + ns.GOOGLE_QUEUE, + ] + + def _cb_bare_jid_disco_iq(self, iq): + # Google talk doesn't support PEP :( + iq['type'] = 'result' + iq['from'] = iq['to'] + self.send(iq) + + +def make_connection(bus, event_func, params=None, suffix=''): + # Gabble accepts a resource in 'account', but the value of 'resource' + # overrides it if there is one. + test_name = re.sub('(.*tests/twisted/|\./)', '', sys.argv[0]) + account = 'test%s@localhost/%s' % (suffix, test_name) + + default_params = { + 'account': account, + 'password': 'pass', + 'resource': 'Resource', + 'server': 'localhost', + 'port': dbus.UInt32(4242), + 'fallback-socks5-proxies': dbus.Array([], signature='s'), + 'require-encryption': False, + } + + if params: + default_params.update(params) + + # Allow omitting the 'password' param + if default_params['password'] is None: + del default_params['password'] + + # Allow omitting the 'account' param + if default_params['account'] is None: + del default_params['account'] + + jid = default_params.get('account', None) + conn = servicetest.make_connection(bus, event_func, 'gabble', 'jabber', + default_params) + return (conn, jid) + +def make_stream(event_func, authenticator=None, protocol=None, + resource=None, suffix=''): + # set up Jabber server + if authenticator is None: + authenticator = XmppAuthenticator('test%s' % suffix, 'pass', resource=resource) + + authenticator.set_event_func(event_func) + + if protocol is None: + protocol = XmppXmlStream + + stream = protocol(event_func, authenticator) + return stream + +def disconnect_conn(q, conn, stream, expected_before=[], expected_after=[]): + call_async(q, conn, 'Disconnect') + + tmp = expected_before + [ + EventPattern('dbus-signal', signal='StatusChanged', args=[cs.CONN_STATUS_DISCONNECTED, cs.CSR_REQUESTED]), + EventPattern('stream-closed')] + + before_events = q.expect_many(*tmp) + + stream.sendFooter() + + tmp = expected_after + [EventPattern('dbus-return', method='Disconnect')] + after_events = q.expect_many(*tmp) + + return before_events[:-2], after_events[:-1] + +def element_repr(element): + """__repr__ cannot safely return non-ASCII: see + <http://bugs.python.org/issue5876>. So we print non-ASCII characters as + \uXXXX escapes in debug output + + """ + return element.toXml().encode('unicode-escape') + +def expect_connected(queue): + queue.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTING, cs.CSR_REQUESTED]) + queue.expect('stream-authenticated') + queue.expect('dbus-signal', signal='PresencesChanged', + args=[{1L: (cs.PRESENCE_AVAILABLE, u'available', '')}]) + queue.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + +def exec_test_deferred(fun, params, protocol=None, timeout=None, + authenticator=None, num_instances=1, + do_connect=True, + make_connection_func=make_connection, + expect_connected_func=expect_connected): + # hack to ease debugging + domish.Element.__repr__ = element_repr + colourer = None + + if sys.stdout.isatty() or 'CHECK_FORCE_COLOR' in os.environ: + colourer = servicetest.install_colourer() + + try: + bus = dbus.SessionBus() + except dbus.exceptions.DBusException as e: + print e + os._exit(1) + + queue = servicetest.IteratingEventQueue(timeout) + queue.verbose = ( + os.environ.get('CHECK_TWISTED_VERBOSE', '') != '' + or '-v' in sys.argv) + + conns = [] + jids = [] + streams = [] + resource = params.get('resource') if params is not None else None + for i in range(0, num_instances): + if i == 0: + suffix = '' + else: + suffix = str(i) + + try: + (conn, jid) = make_connection_func(bus, queue.append, params, suffix) + except Exception, e: + # Crap. This is normally because the connection's still kicking + # around on the bus. Let's bin any connections we *did* manage to + # get going and then bail out unceremoniously. + print e + + for conn in conns: + conn.Disconnect() + + os._exit(1) + + conns.append(conn) + jids.append(jid) + streams.append(make_stream(queue.append, protocol=protocol, + authenticator=authenticator, + resource=resource, suffix=suffix)) + + factory = StreamFactory(streams, jids) + port = reactor.listenTCP(4242, factory, interface='localhost') + + def signal_receiver(*args, **kw): + if kw['path'] == '/org/freedesktop/DBus' and \ + kw['member'] == 'NameOwnerChanged': + bus_name, old_name, new_name = args + if new_name == '': + for i, conn in enumerate(conns): + stream = streams[i] + jid = jids[i] + if conn._requested_bus_name == bus_name: + factory.lost_presence(stream, jid) + break + queue.append(Event('dbus-signal', + path=unwrap(kw['path']), + signal=kw['member'], args=map(unwrap, args), + interface=kw['interface'])) + + match_all_signals = 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: + if do_connect: + for conn in conns: + conn.Connect() + expect_connected_func(queue) + + if len(conns) == 1: + fun(queue, bus, conns[0], streams[0]) + else: + fun(queue, bus, conns, streams) + except Exception, e: + traceback.print_exc() + error = e + queue.verbose = False + + if colourer: + sys.stdout = colourer.fh + + d = port.stopListening() + + # Does the Connection object still exist? + for i, conn in enumerate(conns): + if not bus.name_has_owner(conn.object.bus_name): + # Connection has already been disconnected and destroyed + continue + try: + if conn.GetStatus() == cs.CONN_STATUS_CONNECTED: + # Connection is connected, properly disconnect it + disconnect_conn(queue, conn, streams[i]) + else: + # Connection is not connected, call Disconnect() to destroy it + conn.Disconnect() + except dbus.DBusException, e: + pass + except Exception, e: + traceback.print_exc() + error = e + + try: + conn.Disconnect() + raise AssertionError("Connection didn't disappear; " + "all subsequent tests will probably fail") + except dbus.DBusException, e: + pass + except Exception, e: + traceback.print_exc() + error = e + + match_all_signals.remove() + + if error is None: + d.addBoth((lambda *args: reactor.crash())) + else: + # please ignore the POSIX behind the curtain + d.addBoth((lambda *args: os._exit(1))) + + +def exec_test(fun, params=None, protocol=None, timeout=None, + authenticator=None, num_instances=1, do_connect=True): + reactor.callWhenRunning( + exec_test_deferred, fun, params, protocol, timeout, authenticator, num_instances, + do_connect) + reactor.run() + +# Useful routines for server-side vCard handling +current_vcard = domish.Element(('vcard-temp', 'vCard')) + +def expect_and_handle_get_vcard(q, stream): + get_vcard_event = q.expect('stream-iq', query_ns=ns.VCARD_TEMP, + query_name='vCard', iq_type='get') + + iq = get_vcard_event.stanza + vcard = iq.firstChildElement() + assert vcard.name == 'vCard', vcard.toXml() + + # Send back current vCard + result = make_result_iq(stream, iq, add_query_node=False) + result.addChild(current_vcard) + stream.send(result) + +def expect_and_handle_set_vcard(q, stream, check=None): + global current_vcard + set_vcard_event = q.expect('stream-iq', query_ns=ns.VCARD_TEMP, + query_name='vCard', iq_type='set') + iq = set_vcard_event.stanza + vcard = iq.firstChildElement() + assert vcard.name == 'vCard', vcard.toXml() + + if check is not None: + check(vcard) + + # Update current vCard + current_vcard = vcard + + stream.send(make_result_iq(stream, iq)) + +def _elem_add(elem, *children): + for child in children: + if isinstance(child, domish.Element): + elem.addChild(child) + elif isinstance(child, unicode): + elem.addContent(child) + else: + raise ValueError( + 'invalid child object %r (must be element or unicode)', child) + +def elem(a, b=None, attrs={}, **kw): + r""" + >>> elem('foo')().toXml() + u'<foo/>' + >>> elem('foo', x='1')().toXml() + u"<foo x='1'/>" + >>> elem('foo', x='1')(u'hello').toXml() + u"<foo x='1'>hello</foo>" + >>> elem('foo', x='1')(u'hello', + ... elem('http://foo.org', 'bar', y='2')(u'bye')).toXml() + u"<foo x='1'>hello<bar xmlns='http://foo.org' y='2'>bye</bar></foo>" + >>> elem('foo', attrs={'xmlns:bar': 'urn:bar', 'bar:cake': 'yum'})( + ... elem('bar:e')(u'i') + ... ).toXml() + u"<foo xmlns:bar='urn:bar' bar:cake='yum'><bar:e>i</bar:e></foo>" + """ + + class _elem(domish.Element): + def __call__(self, *children): + _elem_add(self, *children) + return self + + if b is not None: + elem = _elem((a, b)) + else: + elem = _elem((None, a)) + + # Can't just update kw into attrs, because that *modifies the parameter's + # default*. Thanks python. + allattrs = {} + allattrs.update(kw) + allattrs.update(attrs) + + # First, let's pull namespaces out + realattrs = {} + for k, v in allattrs.iteritems(): + if k.startswith('xmlns:'): + abbr = k[len('xmlns:'):] + elem.localPrefixes[abbr] = v + else: + realattrs[k] = v + + for k, v in realattrs.iteritems(): + if k == 'from_': + elem['from'] = v + else: + elem[k] = v + + return elem + +def elem_iq(server, type, **kw): + class _iq(IQ): + def __call__(self, *children): + _elem_add(self, *children) + return self + + iq = _iq(server, type) + + for k, v in kw.iteritems(): + if k == 'from_': + iq['from'] = v + else: + iq[k] = v + + return iq + +def make_presence(_from, to='test@localhost', type=None, show=None, + status=None, caps=None, photo=None): + presence = domish.Element((None, 'presence')) + presence['from'] = _from + presence['to'] = to + + if type is not None: + presence['type'] = type + + if show is not None: + presence.addElement('show', content=show) + + if status is not None: + presence.addElement('status', content=status) + + if caps is not None: + cel = presence.addElement(('http://jabber.org/protocol/caps', 'c')) + for key,value in caps.items(): + cel[key] = value + + # <x xmlns="vcard-temp:x:update"><photo>4a1...</photo></x> + if photo is not None: + x = presence.addElement((ns.VCARD_TEMP_UPDATE, 'x')) + x.addElement('photo').addContent(photo) + + return presence diff --git a/tests/twisted/hazetest.py b/tests/twisted/hazetest.py index 90c3422..7df7f3d 100644 --- a/tests/twisted/hazetest.py +++ b/tests/twisted/hazetest.py @@ -1,439 +1,19 @@ - """ Infrastructure code for testing Haze by pretending to be a Jabber server. - -This is based on gabbletest.py in telepathy-gabble. Haze-specific hacks should -be marked with an 'XXX Haze' comment. This offends me too, but I don't have -time to do anything better. """ -import base64 -import os -import hashlib import sys -import random -import re -import traceback - -import ns -import constants as cs -import servicetest -from servicetest import ( - assertEquals, assertLength, assertContains, wrap_channel, - EventPattern, call_async, unwrap, Event) -import twisted from twisted.words.xish import domish, xpath -from twisted.words.protocols.jabber.client import IQ -from twisted.words.protocols.jabber import xmlstream -from twisted.internet import reactor, ssl - import dbus -def make_result_iq(stream, iq, add_query_node=True): - result = IQ(stream, "result") - result["id"] = iq["id"] - to = iq.getAttribute('to') - if to is not None: - result["from"] = to - query = iq.firstChildElement() - - if query and add_query_node: - result.addElement((query.uri, query.name)) - - return result - -def acknowledge_iq(stream, iq): - stream.send(make_result_iq(stream, iq)) - -def send_error_reply(stream, iq, error_stanza=None): - result = IQ(stream, "error") - result["id"] = iq["id"] - query = iq.firstChildElement() - to = iq.getAttribute('to') - if to is not None: - result["from"] = to - - if query: - result.addElement((query.uri, query.name)) - - if error_stanza: - result.addChild(error_stanza) - - stream.send(result) - -def request_muc_handle(q, conn, stream, muc_jid): - servicetest.call_async(q, conn, 'RequestHandles', 2, [muc_jid]) - event = q.expect('dbus-return', method='RequestHandles') - return event.value[0][0] - -def make_muc_presence(affiliation, role, muc_jid, alias, jid=None): - presence = domish.Element((None, 'presence')) - presence['from'] = '%s/%s' % (muc_jid, alias) - x = presence.addElement((ns.MUC_USER, 'x')) - item = x.addElement('item') - item['affiliation'] = affiliation - item['role'] = role - if jid is not None: - item['jid'] = jid - return presence - -def sync_stream(q, stream): - """Used to ensure that Gabble has processed all stanzas sent to it.""" - - iq = IQ(stream, "get") - id = iq['id'] - iq.addElement(('http://jabber.org/protocol/disco#info', 'query')) - stream.send(iq) - q.expect('stream-iq', query_ns='http://jabber.org/protocol/disco#info', - predicate=(lambda event: - event.stanza['id'] == id and event.iq_type == 'result')) - -class GabbleAuthenticator(xmlstream.Authenticator): - def __init__(self, username, password, resource=None): - self.username = username - self.password = password - self.resource = resource - self.bare_jid = None - self.full_jid = None - xmlstream.Authenticator.__init__(self) - -class JabberAuthenticator(GabbleAuthenticator): - "Trivial XML stream authenticator that accepts one username/digest pair." - - # Patch in fix from http://twistedmatrix.com/trac/changeset/23418. - # This monkeypatch taken from Gadget source code - from twisted.words.xish.utility import EventDispatcher - - def _addObserver(self, onetime, event, observerfn, priority, *args, - **kwargs): - if self._dispatchDepth > 0: - self._updateQueue.append(lambda: self._addObserver(onetime, event, - observerfn, priority, *args, **kwargs)) - - return self._oldAddObserver(onetime, event, observerfn, priority, - *args, **kwargs) - - EventDispatcher._oldAddObserver = EventDispatcher._addObserver - EventDispatcher._addObserver = _addObserver - - def streamStarted(self, root=None): - if root: - self.xmlstream.sid = '%x' % random.randint(1, sys.maxint) - - self.xmlstream.sendHeader() - self.xmlstream.addOnetimeObserver( - "/iq/query[@xmlns='jabber:iq:auth']", self.initialIq) - - def initialIq(self, iq): - result = IQ(self.xmlstream, "result") - result["id"] = iq["id"] - query = result.addElement('query') - query["xmlns"] = "jabber:iq:auth" - query.addElement('username', content='test') - query.addElement('password') - query.addElement('digest') - query.addElement('resource') - self.xmlstream.addOnetimeObserver('/iq/query/username', self.secondIq) - self.xmlstream.send(result) - - def secondIq(self, iq): - username = xpath.queryForNodes('/iq/query/username', iq) - assert map(str, username) == [self.username] - - digest = xpath.queryForNodes('/iq/query/digest', iq) - expect = hashlib.sha1(self.xmlstream.sid + self.password).hexdigest() - assert map(str, digest) == [expect] - - resource = xpath.queryForNodes('/iq/query/resource', iq) - assertLength(1, resource) - if self.resource is not None: - assertEquals(self.resource, str(resource[0])) - - self.bare_jid = '%s@localhost' % self.username - self.full_jid = '%s/%s' % (self.bare_jid, resource) - - result = IQ(self.xmlstream, "result") - result["id"] = iq["id"] - self.xmlstream.send(result) - self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) - -class XmppAuthenticator(GabbleAuthenticator): - def __init__(self, username, password, resource=None): - GabbleAuthenticator.__init__(self, username, password, resource) - self.authenticated = False - - def streamInitialize(self, root): - if root: - self.xmlstream.sid = root.getAttribute('id') - - if self.xmlstream.sid is None: - self.xmlstream.sid = '%x' % random.randint(1, sys.maxint) - - self.xmlstream.sendHeader() - - def streamIQ(self): - features = elem(xmlstream.NS_STREAMS, 'features')( - elem(ns.NS_XMPP_BIND, 'bind'), - elem(ns.NS_XMPP_SESSION, 'session'), - ) - self.xmlstream.send(features) - - self.xmlstream.addOnetimeObserver( - "/iq/bind[@xmlns='%s']" % ns.NS_XMPP_BIND, self.bindIq) - self.xmlstream.addOnetimeObserver( - "/iq/session[@xmlns='%s']" % ns.NS_XMPP_SESSION, self.sessionIq) - - def streamSASL(self): - features = domish.Element((xmlstream.NS_STREAMS, 'features')) - mechanisms = features.addElement((ns.NS_XMPP_SASL, 'mechanisms')) - mechanism = mechanisms.addElement('mechanism', content='PLAIN') - self.xmlstream.send(features) - - self.xmlstream.addOnetimeObserver("/auth", self.auth) - - def streamStarted(self, root=None): - self.streamInitialize(root) - - if self.authenticated: - # Initiator authenticated itself, and has started a new stream. - self.streamIQ() - else: - self.streamSASL() - - def auth(self, auth): - assert (base64.b64decode(str(auth)) == - '\x00%s\x00%s' % (self.username, self.password)) - - success = domish.Element((ns.NS_XMPP_SASL, 'success')) - self.xmlstream.send(success) - self.xmlstream.reset() - self.authenticated = True - - def bindIq(self, iq): - resource = xpath.queryForString('/iq/bind/resource', iq) - if self.resource is not None: - assertEquals(self.resource, resource) - else: - assert resource is not None - - result = IQ(self.xmlstream, "result") - result["id"] = iq["id"] - bind = result.addElement((ns.NS_XMPP_BIND, 'bind')) - self.bare_jid = '%s@localhost' % self.username - self.full_jid = '%s/%s' % (self.bare_jid, resource) - jid = bind.addElement('jid', content=self.full_jid) - self.xmlstream.send(result) - - self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) - - def sessionIq(self, iq): - self.xmlstream.send(make_result_iq(self.xmlstream, iq)) - -def make_stream_event(type, stanza, stream): - event = servicetest.Event(type, stanza=stanza) - event.stream = stream - event.to = stanza.getAttribute("to") - return event - -def make_iq_event(stream, iq): - event = make_stream_event('stream-iq', iq, stream) - event.iq_type = iq.getAttribute("type") - event.iq_id = iq.getAttribute("id") - query = iq.firstChildElement() - - if query: - event.query = query - event.query_ns = query.uri - event.query_name = query.name - - if query.getAttribute("node"): - event.query_node = query.getAttribute("node") - else: - event.query = None - - return event - -def make_presence_event(stream, stanza): - event = make_stream_event('stream-presence', stanza, stream) - event.presence_type = stanza.getAttribute('type') - - statuses = xpath.queryForNodes('/presence/status', stanza) - - if statuses: - event.presence_status = str(statuses[0]) - - return event - -def make_message_event(stream, stanza): - event = make_stream_event('stream-message', stanza, stream) - event.message_type = stanza.getAttribute('type') - return event - -class StreamFactory(twisted.internet.protocol.Factory): - def __init__(self, streams, jids): - self.streams = streams - self.jids = jids - self.presences = {} - self.mappings = dict(map (lambda jid, stream: (jid, stream), - jids, streams)) - - # Make a copy of the streams - self.factory_streams = list(streams) - self.factory_streams.reverse() - - # Do not add observers for single instances because it's unnecessary and - # some unit tests need to respond to the roster request, and we shouldn't - # answer it for them otherwise we break compatibility - if len(streams) > 1: - # We need to have a function here because lambda keeps a reference on - # the stream and jid and in the for loop, there is no context - def addObservers(stream, jid): - stream.addObserver('/iq', lambda x: \ - self.forward_iq(stream, jid, x)) - stream.addObserver('/presence', lambda x: \ - self.got_presence(stream, jid, x)) - - for (jid, stream) in self.mappings.items(): - addObservers(stream, jid) - - def protocol(self, *args): - return self.factory_streams.pop() - - - def got_presence (self, stream, jid, stanza): - stanza.attributes['from'] = jid - self.presences[jid] = stanza - - for dest_jid in self.presences.keys(): - # Dispatch the new presence to other clients - stanza.attributes['to'] = dest_jid - self.mappings[dest_jid].send(stanza) - - # Don't echo the presence twice - if dest_jid != jid: - # Dispatch other client's presence to this stream - presence = self.presences[dest_jid] - presence.attributes['to'] = jid - stream.send(presence) - - def lost_presence(self, stream, jid): - if self.presences.has_key(jid): - del self.presences[jid] - for dest_jid in self.presences.keys(): - presence = domish.Element(('jabber:client', 'presence')) - presence['from'] = jid - presence['to'] = dest_jid - presence['type'] = 'unavailable' - self.mappings[dest_jid].send(presence) - - def forward_iq(self, stream, jid, stanza): - stanza.attributes['from'] = jid - - query = stanza.firstChildElement() - - # Fake other accounts as being part of our roster - if query and query.uri == ns.ROSTER: - roster = make_result_iq(stream, stanza) - query = roster.firstChildElement() - for roster_jid in self.mappings.keys(): - if jid != roster_jid: - item = query.addElement('item') - item['jid'] = roster_jid - item['subscription'] = 'both' - stream.send(roster) - return - - to = stanza.getAttribute('to') - dest = None - if to is not None: - dest = self.mappings.get(to) - - if dest is not None: - dest.send(stanza) - -class BaseXmlStream(xmlstream.XmlStream): - initiating = False - namespace = 'jabber:client' - pep_support = True - disco_features = [] - handle_privacy_lists = True - - def __init__(self, event_func, authenticator): - xmlstream.XmlStream.__init__(self, authenticator) - self.event_func = event_func - self.addObserver('//iq', lambda x: event_func( - make_iq_event(self, x))) - self.addObserver('//message', lambda x: event_func( - make_message_event(self, x))) - self.addObserver('//presence', lambda x: event_func( - make_presence_event(self, x))) - self.addObserver('//event/stream/authd', self._cb_authd) - if self.handle_privacy_lists: - self.addObserver("/iq/query[@xmlns='%s']" % ns.PRIVACY, - self._cb_priv_list) - - def _cb_priv_list(self, iq): - send_error_reply(self, iq) - - def _cb_authd(self, _): - # called when stream is authenticated - assert self.authenticator.full_jid is not None - assert self.authenticator.bare_jid is not None - - self.addObserver( - "/iq[@to='localhost']/query[@xmlns='http://jabber.org/protocol/disco#info']", - self._cb_disco_iq) - self.addObserver( - "/iq[@to='%s']/query[@xmlns='http://jabber.org/protocol/disco#info']" - % self.authenticator.bare_jid, - self._cb_bare_jid_disco_iq) - # XXX Haze - self.add_roster_observer() - self.event_func(servicetest.Event('stream-authenticated')) - - def _cb_disco_iq(self, iq): - nodes = xpath.queryForNodes( - "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']", iq) - query = nodes[0] - - for feature in self.disco_features: - query.addChild(elem('feature', var=feature)) - - iq['type'] = 'result' - iq['from'] = iq['to'] - self.send(iq) - - def _cb_bare_jid_disco_iq(self, iq): - # advertise PEP support - nodes = xpath.queryForNodes( - "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']", - iq) - query = nodes[0] - identity = query.addElement('identity') - identity['category'] = 'pubsub' - identity['type'] = 'pep' - - iq['type'] = 'result' - iq['from'] = iq['to'] - self.send(iq) - - def onDocumentEnd(self): - self.event_func(servicetest.Event('stream-closed')) - # We don't chain up XmlStream.onDocumentEnd() because it will - # disconnect the TCP connection making tests as - # connect/disconnect-timeout.py not working - - def send_stream_error(self, error='system-shutdown'): - # Yes, there are meant to be two different STREAMS namespaces. - go_away = \ - elem(xmlstream.NS_STREAMS, 'error')( - elem(ns.STREAMS, error) - ) +import servicetest +# reexport everything, but override a thing +from gabbletest import * - self.send(go_away) +class EmptyRosterXmppXmlStream(XmppXmlStream): + def _cb_authd(self, x): + XmppXmlStream._cb_authd(self, x) - # XXX Haze: the next two methods are Haze-specific. - def add_roster_observer(self): self.addObserver( "/iq/query[@xmlns='jabber:iq:roster']", self._cb_roster_get) @@ -444,44 +24,18 @@ class BaseXmlStream(xmlstream.XmlStream): if iq.getAttribute('type') == 'get': self.send(make_result_iq(self, iq)) -class JabberXmlStream(BaseXmlStream): - version = (0, 9) - -class XmppXmlStream(BaseXmlStream): - version = (1, 0) - -class GoogleXmlStream(BaseXmlStream): - version = (1, 0) - - pep_support = False - disco_features = [ns.GOOGLE_ROSTER, - ns.GOOGLE_JINGLE_INFO, - ns.GOOGLE_MAIL_NOTIFY, - ] - - def _cb_bare_jid_disco_iq(self, iq): - # Google talk doesn't support PEP :( - iq['type'] = 'result' - iq['from'] = iq['to'] - self.send(iq) - - -def make_connection(bus, event_func, params=None, suffix=''): +def make_haze_connection(bus, event_func, params=None, suffix=''): # Gabble accepts a resource in 'account', but the value of 'resource' - # overrides it if there is one. - # XXX Haze doesn't. + # overrides it if there is one. Haze doesn't. # account = 'test%s@localhost/%s' % (suffix, re.sub(r'.*/', '', sys.argv[0])) account = 'test%s@localhost/Resource' % (suffix, ) default_params = { 'account': account, 'password': 'pass', - # XXX Haze: fd.o#14212. - #'resource': 'Resource', 'ft-proxies': sys.argv[0], 'server': 'localhost', 'port': dbus.UInt32(4242), - # XXX Haze 'require-encryption': False, 'auth-plain-in-clear': True, } @@ -498,302 +52,29 @@ def make_connection(bus, event_func, params=None, suffix=''): del default_params['account'] jid = default_params.get('account', None) - # XXX Haze conn = servicetest.make_connection(bus, event_func, 'haze', 'jabber', default_params) return (conn, jid) -def make_stream(event_func, authenticator=None, protocol=None, - resource=None, suffix=''): - # set up Jabber server - if authenticator is None: - authenticator = XmppAuthenticator('test%s' % suffix, 'pass', resource=resource) - - if protocol is None: - protocol = XmppXmlStream - - stream = protocol(event_func, authenticator) - return stream - -def disconnect_conn(q, conn, stream, expected_before=[], expected_after=[]): - call_async(q, conn, 'Disconnect') - - tmp = expected_before + [ - EventPattern('dbus-signal', signal='StatusChanged', args=[cs.CONN_STATUS_DISCONNECTED, cs.CSR_REQUESTED]), - EventPattern('stream-closed')] - - before_events = q.expect_many(*tmp) - - stream.sendFooter() - - tmp = expected_after + [EventPattern('dbus-return', method='Disconnect')] - after_events = q.expect_many(*tmp) - - return before_events[:-2], after_events[:-1] - -def exec_test_deferred(fun, params, protocol=None, timeout=None, - authenticator=None, num_instances=1): - # hack to ease debugging - domish.Element.__repr__ = domish.Element.toXml - colourer = None - - if sys.stdout.isatty() or 'CHECK_FORCE_COLOR' in os.environ: - colourer = servicetest.install_colourer() - - bus = dbus.SessionBus() - - queue = servicetest.IteratingEventQueue(timeout) - queue.verbose = ( - os.environ.get('CHECK_TWISTED_VERBOSE', '') != '' - or '-v' in sys.argv) - - conns = [] - jids = [] - streams = [] - resource = params.get('resource') if params is not None else None - for i in range(0, num_instances): - if i == 0: - suffix = '' - else: - suffix = str(i) - - try: - (conn, jid) = make_connection(bus, queue.append, params, suffix) - except Exception, e: - # Crap. This is normally because the connection's still kicking - # around on the bus. Let's bin any connections we *did* manage to - # get going and then bail out unceremoniously. - print e - - for conn in conns: - conn.Disconnect() - - os._exit(1) - - conns.append(conn) - jids.append(jid) - streams.append(make_stream(queue.append, protocol=protocol, - authenticator=authenticator, - resource=resource, suffix=suffix)) - - factory = StreamFactory(streams, jids) - port = reactor.listenTCP(4242, factory) - - def signal_receiver(*args, **kw): - if kw['path'] == '/org/freedesktop/DBus' and \ - kw['member'] == 'NameOwnerChanged': - bus_name, old_name, new_name = args - if new_name == '': - for i, conn in enumerate(conns): - stream = streams[i] - jid = jids[i] - if conn._requested_bus_name == bus_name: - factory.lost_presence(stream, jid) - break - 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: - if len(conns) == 1: - fun(queue, bus, conns[0], streams[0]) - else: - fun(queue, bus, conns, streams) - except Exception, e: - traceback.print_exc() - error = e - - if colourer: - sys.stdout = colourer.fh - - d = port.stopListening() - - # Does the Connection object still exist? - for i, conn in enumerate(conns): - if not bus.name_has_owner(conn.object.bus_name): - # Connection has already been disconnected and destroyed - continue - try: - if conn.GetStatus() == cs.CONN_STATUS_CONNECTED: - # Connection is connected, properly disconnect it - disconnect_conn(queue, conn, streams[i]) - else: - # Connection is not connected, call Disconnect() to destroy it - conn.Disconnect() - except dbus.DBusException, e: - pass - - try: - conn.Disconnect() - raise AssertionError("Connection didn't disappear; " - "all subsequent tests will probably fail") - except dbus.DBusException, e: - pass - except Exception, e: - traceback.print_exc() - error = e - - if error is None: - d.addBoth((lambda *args: reactor.crash())) - else: - # please ignore the POSIX behind the curtain - d.addBoth((lambda *args: os._exit(1))) - - -def exec_test(fun, params=None, protocol=None, timeout=None, - authenticator=None, num_instances=1): +def expect_kinda_connected(queue): + queue.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTING, cs.CSR_REQUESTED]) + queue.expect('stream-authenticated') + # FIXME: unlike Gabble, Haze does not signal a presence update to available + # during connect + # queue.expect('dbus-signal', signal='PresencesChanged', + # args=[{1L: (cs.PRESENCE_AVAILABLE, u'available', '')}]) + queue.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + +# Copy pasta because we need to replace make_connection +def exec_test(fun, params=None, protocol=EmptyRosterXmppXmlStream, timeout=None, + authenticator=None, num_instances=1, do_connect=True): reactor.callWhenRunning( - exec_test_deferred, fun, params, protocol, timeout, authenticator, num_instances) + exec_test_deferred, fun, params, protocol, timeout, authenticator, num_instances, + do_connect, make_haze_connection, expect_kinda_connected) reactor.run() -# Useful routines for server-side vCard handling -current_vcard = domish.Element(('vcard-temp', 'vCard')) - -def expect_and_handle_get_vcard(q, stream): - get_vcard_event = q.expect('stream-iq', query_ns=ns.VCARD_TEMP, - query_name='vCard', iq_type='get') - - iq = get_vcard_event.stanza - vcard = iq.firstChildElement() - assert vcard.name == 'vCard', vcard.toXml() - - # Send back current vCard - result = make_result_iq(stream, iq) - result.addChild(current_vcard) - stream.send(result) - -def expect_and_handle_set_vcard(q, stream, check=None): - set_vcard_event = q.expect('stream-iq', query_ns=ns.VCARD_TEMP, - query_name='vCard', iq_type='set') - iq = set_vcard_event.stanza - vcard = iq.firstChildElement() - assert vcard.name == 'vCard', vcard.toXml() - - if check is not None: - check(vcard) - - # Update current vCard - current_vcard = vcard - - stream.send(make_result_iq(stream, iq)) - -def _elem_add(elem, *children): - for child in children: - if isinstance(child, domish.Element): - elem.addChild(child) - elif isinstance(child, unicode): - elem.addContent(child) - else: - raise ValueError( - 'invalid child object %r (must be element or unicode)', child) - -def elem(a, b=None, attrs={}, **kw): - r""" - >>> elem('foo')().toXml() - u'<foo/>' - >>> elem('foo', x='1')().toXml() - u"<foo x='1'/>" - >>> elem('foo', x='1')(u'hello').toXml() - u"<foo x='1'>hello</foo>" - >>> elem('foo', x='1')(u'hello', - ... elem('http://foo.org', 'bar', y='2')(u'bye')).toXml() - u"<foo x='1'>hello<bar xmlns='http://foo.org' y='2'>bye</bar></foo>" - >>> elem('foo', attrs={'xmlns:bar': 'urn:bar', 'bar:cake': 'yum'})( - ... elem('bar:e')(u'i') - ... ).toXml() - u"<foo xmlns:bar='urn:bar' bar:cake='yum'><bar:e>i</bar:e></foo>" - """ - - class _elem(domish.Element): - def __call__(self, *children): - _elem_add(self, *children) - return self - - if b is not None: - elem = _elem((a, b)) - else: - elem = _elem((None, a)) - - # Can't just update kw into attrs, because that *modifies the parameter's - # default*. Thanks python. - allattrs = {} - allattrs.update(kw) - allattrs.update(attrs) - - # First, let's pull namespaces out - realattrs = {} - for k, v in allattrs.iteritems(): - if k.startswith('xmlns:'): - abbr = k[len('xmlns:'):] - elem.localPrefixes[abbr] = v - else: - realattrs[k] = v - - for k, v in realattrs.iteritems(): - if k == 'from_': - elem['from'] = v - else: - elem[k] = v - - return elem - -def elem_iq(server, type, **kw): - class _iq(IQ): - def __call__(self, *children): - _elem_add(self, *children) - return self - - iq = _iq(server, type) - - for k, v in kw.iteritems(): - if k == 'from_': - iq['from'] = v - else: - iq[k] = v - - return iq - -def make_presence(_from, to='test@localhost', type=None, show=None, - status=None, caps=None, photo=None): - presence = domish.Element((None, 'presence')) - presence['from'] = _from - presence['to'] = to - - if type is not None: - presence['type'] = type - - if show is not None: - presence.addElement('show', content=show) - - if status is not None: - presence.addElement('status', content=status) - - if caps is not None: - cel = presence.addElement(('http://jabber.org/protocol/caps', 'c')) - for key,value in caps.items(): - cel[key] = value - - # <x xmlns="vcard-temp:x:update"><photo>4a1...</photo></x> - if photo is not None: - x = presence.addElement((ns.VCARD_TEMP_UPDATE, 'x')) - x.addElement('photo').addContent(photo) - - return presence - def close_all_groups(q, bus, conn, stream): channels = conn.Properties.Get(cs.CONN_IFACE_REQUESTS, 'Channels') for path, props in channels: @@ -803,3 +84,4 @@ def close_all_groups(q, bus, conn, stream): continue wrap_channel(bus.get_object(conn.bus_name, path), cs.CHANNEL_TYPE_CONTACT_LIST).Close() + diff --git a/tests/twisted/presence/presence.py b/tests/twisted/presence/presence.py index dea36a3..a3c5854 100644 --- a/tests/twisted/presence/presence.py +++ b/tests/twisted/presence/presence.py @@ -12,9 +12,6 @@ from twisted.words.protocols.jabber.client import IQ from hazetest import exec_test def test(q, bus, conn, stream): - conn.Connect() - q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) - amy_handle = conn.RequestHandles(1, ['amy@foo.com'])[0] # Divergence from Gabble: hazetest responds to all roster gets with an diff --git a/tests/twisted/roster/groups.py b/tests/twisted/roster/groups.py index 9b3bd02..8f8f2c9 100644 --- a/tests/twisted/roster/groups.py +++ b/tests/twisted/roster/groups.py @@ -14,9 +14,6 @@ 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]) self_handle = conn.GetSelfHandle() # Close all Group channels to get a clean slate, so we can rely on diff --git a/tests/twisted/roster/initial-roster.py b/tests/twisted/roster/initial-roster.py index 61b4731..f745bb6 100644 --- a/tests/twisted/roster/initial-roster.py +++ b/tests/twisted/roster/initial-roster.py @@ -10,11 +10,6 @@ from servicetest import (assertLength, EventPattern, wrap_channel, 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() @@ -140,4 +135,4 @@ def test(q, bus, conn, stream): assertEquals(default_props, e.value[2]) if __name__ == '__main__': - exec_test(test, protocol=RosterXmlStream) + exec_test(test, protocol=JabberXmlStream, do_connect=False) diff --git a/tests/twisted/roster/publish.py b/tests/twisted/roster/publish.py index 02eec95..f04abca 100644 --- a/tests/twisted/roster/publish.py +++ b/tests/twisted/roster/publish.py @@ -14,10 +14,6 @@ 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]) - # Close all Group channels to get a clean slate, so we can rely on # the NewChannels signal for the default group later close_all_groups(q, bus, conn, stream) diff --git a/tests/twisted/roster/removed-from-rp-subscribe.py b/tests/twisted/roster/removed-from-rp-subscribe.py index 522255e..8550cba 100644 --- a/tests/twisted/roster/removed-from-rp-subscribe.py +++ b/tests/twisted/roster/removed-from-rp-subscribe.py @@ -13,10 +13,6 @@ 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, diff --git a/tests/twisted/roster/subscribe.py b/tests/twisted/roster/subscribe.py index ba20cdd..f6225de 100644 --- a/tests/twisted/roster/subscribe.py +++ b/tests/twisted/roster/subscribe.py @@ -13,9 +13,6 @@ 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]) self_handle = conn.GetSelfHandle() # Close all Group channels to get a clean slate, so we can rely on diff --git a/tests/twisted/servicetest.py b/tests/twisted/servicetest.py index 1832270..31c00f4 100644 --- a/tests/twisted/servicetest.py +++ b/tests/twisted/servicetest.py @@ -8,6 +8,7 @@ from twisted.internet.protocol import Protocol, Factory, ClientFactory glib2reactor.install() import sys import time +import os import pprint import unittest @@ -21,16 +22,38 @@ import constants as cs tp_name_prefix = 'org.freedesktop.Telepathy' tp_path_prefix = '/org/freedesktop/Telepathy' -class Event: +class DictionarySupersetOf (object): + """Utility class for expecting "a dictionary with at least these keys".""" + def __init__(self, dictionary): + self._dictionary = dictionary + def __repr__(self): + return "DictionarySupersetOf(%s)" % self._dictionary + def __eq__(self, other): + """would like to just do: + return set(other.items()).issuperset(self._dictionary.items()) + but it turns out that this doesn't work if you have another dict + nested in the values of your dicts""" + try: + for k,v in self._dictionary.items(): + if k not in other or other[k] != v: + return False + return True + except TypeError: # other is not iterable + return False + +class Event(object): def __init__(self, type, **kw): self.__dict__.update(kw) self.type = type (self.subqueue, self.subtype) = type.split ("-", 1) + def __str__(self): + return '\n'.join([ str(type(self)) ] + format_event(self)) + def format_event(event): ret = ['- type %s' % event.type] - for key in dir(event): + for key in sorted(dir(event)): if key != 'type' and not key.startswith('_'): ret.append('- %s: %s' % ( key, pprint.pformat(getattr(event, key)))) @@ -79,6 +102,14 @@ class EventPattern: class TimeoutError(Exception): pass +class ForbiddenEventOccurred(Exception): + def __init__(self, event): + Exception.__init__(self) + self.event = event + + def __str__(self): + return '\n' + '\n'.join(format_event(self.event)) + class BaseEventQueue: """Abstract event queue base class. @@ -124,13 +155,16 @@ class BaseEventQueue: """ self.forbidden_events.difference_update(set(patterns)) + def unforbid_all(self): + """ + Remove all patterns from the set of forbidden events. + """ + self.forbidden_events.clear() + 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 + raise ForbiddenEventOccurred(event) def expect(self, type, **kw): """ @@ -390,16 +424,21 @@ def call_async(test, proxy, method, *args, **kw): method_proxy(*args, **kw) def sync_dbus(bus, q, conn): - # Dummy D-Bus method call + # Dummy D-Bus method call. We can't use DBus.Peer.Ping() because libdbus + # replies to that message immediately, rather than handing it up to + # dbus-glib and thence Gabble, which means that Ping()ing Gabble doesn't + # ensure that it's processed all D-Bus messages prior to our ping. + # # 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') + root_object = bus.get_object(conn.object.bus_name, '/', introspect=False) + call_async(q, + dbus.Interface(root_object, 'org.freedesktop.Telepathy.Tests'), + 'DummySyncDBus') + q.expect('dbus-error', method='DummySyncDBus') class ProxyWrapper: - def __init__(self, object, default, others): + def __init__(self, object, default, others={}): self.object = object self.default_interface = dbus.Interface(object, default) self.Properties = dbus.Interface(object, dbus.PROPERTIES_IFACE) @@ -423,7 +462,7 @@ def wrap_connection(conn): dict([ (name, tp_name_prefix + '.Connection.Interface.' + name) for name in ['Aliasing', 'Avatars', 'Capabilities', 'Contacts', - 'Presence', 'SimplePresence', 'Requests']] + + 'SimplePresence', 'Requests']] + [('Peer', 'org.freedesktop.DBus.Peer'), ('ContactCapabilities', cs.CONN_IFACE_CONTACT_CAPS), ('ContactInfo', cs.CONN_IFACE_CONTACT_INFO), @@ -433,6 +472,7 @@ def wrap_connection(conn): ('ContactList', cs.CONN_IFACE_CONTACT_LIST), ('ContactGroups', cs.CONN_IFACE_CONTACT_GROUPS), ('PowerSaving', cs.CONN_IFACE_POWER_SAVING), + ('Addressing', cs.CONN_IFACE_ADDRESSING), ])) def wrap_channel(chan, type_, extra=None): @@ -448,14 +488,26 @@ def wrap_channel(chan, type_, extra=None): return ProxyWrapper(chan, tp_name_prefix + '.Channel', interfaces) + +def wrap_content(chan, extra=None): + interfaces = { } + + if extra: + interfaces.update(dict([ + (name, tp_name_prefix + '.Call1.Content.Interface.' + name) + for name in extra])) + + return ProxyWrapper(chan, tp_name_prefix + '.Call1.Content', interfaces) + def make_connection(bus, event_func, name, proto, params): cm = bus.get_object( tp_name_prefix + '.ConnectionManager.%s' % name, - tp_path_prefix + '/ConnectionManager/%s' % name) + tp_path_prefix + '/ConnectionManager/%s' % name, + introspect=False) cm_iface = dbus.Interface(cm, tp_name_prefix + '.ConnectionManager') connection_name, connection_path = cm_iface.RequestConnection( - proto, params) + proto, dbus.Dictionary(params, signature='sv')) conn = wrap_connection(bus.get_object(connection_name, connection_path)) return conn @@ -568,6 +620,12 @@ def assertFlagsUnset(flags, value): "expected none of flags %u, but %u are set in %u" % ( flags, masked, value)) +def assertDBusError(name, error): + if error.get_dbus_name() != name: + raise AssertionError( + "expected DBus error named:\n %s\ngot:\n %s\n(with message: %s)" + % (name, error.get_dbus_name(), error.message)) + def install_colourer(): def red(s): return '\x1b[31m%s\x1b[0m' % s @@ -596,6 +654,16 @@ def install_colourer(): sys.stdout = Colourer(sys.stdout, patterns) return sys.stdout -if __name__ == '__main__': - unittest.main() +# this is just to shut up unittest. +class DummyStream(object): + def write(self, s): + if 'CHECK_TWISTED_VERBOSE' in os.environ: + print s, + def flush(self): + pass + +if __name__ == '__main__': + stream = DummyStream() + runner = unittest.TextTestRunner(stream=stream) + unittest.main(testRunner=runner) diff --git a/tests/twisted/simple-caps.py b/tests/twisted/simple-caps.py index a16f551..d0088d3 100644 --- a/tests/twisted/simple-caps.py +++ b/tests/twisted/simple-caps.py @@ -6,7 +6,7 @@ Make sure ContactCaps works well enough. from twisted.words.xish import domish from servicetest import assertEquals, assertContains, EventPattern -from hazetest import exec_test, sync_stream +from hazetest import exec_test, sync_stream, JabberXmlStream import constants as cs import config @@ -34,10 +34,6 @@ def check_rccs(conn, handle): # do the self handle which will just be text def test_self_handle(q, bus, conn, stream): - conn.Connect() - q.expect('dbus-signal', signal='StatusChanged', - args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) - self_handle = conn.Properties.Get(cs.CONN, 'SelfHandle') check_rccs(conn, self_handle) @@ -71,10 +67,6 @@ def test_someone_else(q, bus, conn, stream): check_rccs(conn, amy_handle) def test_media(q, bus, conn, stream): - conn.Connect() - q.expect('dbus-signal', signal='StatusChanged', - args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) - sync_stream(q, stream) conn.ContactCapabilities.UpdateCapabilities([( @@ -98,7 +90,7 @@ def test_media(q, bus, conn, stream): if __name__ == '__main__': exec_test(test_self_handle) - exec_test(test_someone_else) + exec_test(test_someone_else, do_connect=False, protocol=JabberXmlStream) if config.MEDIA_ENABLED: exec_test(test_media) diff --git a/tests/twisted/text/destroy.py b/tests/twisted/text/destroy.py index cd3805b..1b5e8fb 100644 --- a/tests/twisted/text/destroy.py +++ b/tests/twisted/text/destroy.py @@ -11,9 +11,6 @@ from hazetest import exec_test from servicetest import call_async, EventPattern, assertEquals def test(q, bus, conn, stream): - conn.Connect() - q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) - self_handle = conn.GetSelfHandle() jid = 'foo@bar.com' diff --git a/tests/twisted/text/ensure.py b/tests/twisted/text/ensure.py index ee303e8..21876c3 100644 --- a/tests/twisted/text/ensure.py +++ b/tests/twisted/text/ensure.py @@ -14,9 +14,6 @@ import pprint import constants as cs def test(q, bus, conn, stream): - conn.Connect() - q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) - self_handle = conn.GetSelfHandle() jids = ['foo@bar.com', 'truc@cafe.fr'] diff --git a/tests/twisted/text/initiate-requestotron.py b/tests/twisted/text/initiate-requestotron.py index 867f6c0..a116d2c 100644 --- a/tests/twisted/text/initiate-requestotron.py +++ b/tests/twisted/text/initiate-requestotron.py @@ -12,9 +12,6 @@ from servicetest import call_async, EventPattern import constants as cs def test(q, bus, conn, stream): - conn.Connect() - q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) - self_handle = conn.GetSelfHandle() jid = 'foo@bar.com' diff --git a/tests/twisted/text/initiate.py b/tests/twisted/text/initiate.py index 3426049..88d8117 100644 --- a/tests/twisted/text/initiate.py +++ b/tests/twisted/text/initiate.py @@ -10,9 +10,6 @@ from hazetest import exec_test from servicetest import call_async, EventPattern def test(q, bus, conn, stream): - conn.Connect() - q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) - self_handle = conn.GetSelfHandle() jid = 'foo@bar.com' diff --git a/tests/twisted/text/respawn.py b/tests/twisted/text/respawn.py index f716676..baf5a3d 100644 --- a/tests/twisted/text/respawn.py +++ b/tests/twisted/text/respawn.py @@ -10,9 +10,6 @@ from hazetest import exec_test from servicetest import call_async, EventPattern, assertEquals def test(q, bus, conn, stream): - conn.Connect() - q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) - self_handle = conn.GetSelfHandle() jid = 'foo@bar.com' diff --git a/tests/twisted/text/test-text-delayed.py b/tests/twisted/text/test-text-delayed.py index f2c1a94..90f9f98 100644 --- a/tests/twisted/text/test-text-delayed.py +++ b/tests/twisted/text/test-text-delayed.py @@ -11,9 +11,6 @@ from hazetest import exec_test from servicetest import EventPattern def test(q, bus, conn, stream): - conn.Connect() - q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) - m = domish.Element((None, 'message')) m['from'] = 'foo@bar.com' m['type'] = 'chat' diff --git a/tests/twisted/text/test-text-no-body.py b/tests/twisted/text/test-text-no-body.py index 5de00c2..a3c8ad5 100644 --- a/tests/twisted/text/test-text-no-body.py +++ b/tests/twisted/text/test-text-no-body.py @@ -11,9 +11,6 @@ from hazetest import exec_test import ns def test(q, bus, conn, stream): - conn.Connect() - q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) - # message without body m = domish.Element((None, 'message')) m['from'] = 'alice@foo.com' diff --git a/tests/twisted/text/test-text.py b/tests/twisted/text/test-text.py index 3e4ee36..728491f 100644 --- a/tests/twisted/text/test-text.py +++ b/tests/twisted/text/test-text.py @@ -11,9 +11,6 @@ from hazetest import exec_test from servicetest import EventPattern def test(q, bus, conn, stream): - conn.Connect() - q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) - # <message type="chat"><body>hello</body</message> m = domish.Element((None, 'message')) m['from'] = 'foo@bar.com/Pidgin' |