summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWill Thompson <will.thompson@collabora.co.uk>2013-04-11 19:30:32 +0100
committerWill Thompson <will.thompson@collabora.co.uk>2013-04-11 19:42:30 +0100
commit9ee8b96ad91af52936f5bee5b9a472de645c2363 (patch)
tree876b785a48c06908ae10e5b7c4f2e60b1a84fecf
parent4068d8504b2155168b82144a4d45b138e0579ef2 (diff)
downloadtelepathy-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.
-rw-r--r--tests/twisted/avatar-requirements.py2
-rw-r--r--tests/twisted/cm/protocols.py5
-rw-r--r--tests/twisted/connect/fail.py2
-rw-r--r--tests/twisted/connect/success.py15
-rw-r--r--tests/twisted/connect/twice-to-same-account.py13
-rw-r--r--tests/twisted/constants.py182
-rw-r--r--tests/twisted/gabbletest.py856
-rw-r--r--tests/twisted/hazetest.py768
-rw-r--r--tests/twisted/presence/presence.py3
-rw-r--r--tests/twisted/roster/groups.py3
-rw-r--r--tests/twisted/roster/initial-roster.py7
-rw-r--r--tests/twisted/roster/publish.py4
-rw-r--r--tests/twisted/roster/removed-from-rp-subscribe.py4
-rw-r--r--tests/twisted/roster/subscribe.py3
-rw-r--r--tests/twisted/servicetest.py102
-rw-r--r--tests/twisted/simple-caps.py12
-rw-r--r--tests/twisted/text/destroy.py3
-rw-r--r--tests/twisted/text/ensure.py3
-rw-r--r--tests/twisted/text/initiate-requestotron.py3
-rw-r--r--tests/twisted/text/initiate.py3
-rw-r--r--tests/twisted/text/respawn.py3
-rw-r--r--tests/twisted/text/test-text-delayed.py3
-rw-r--r--tests/twisted/text/test-text-no-body.py3
-rw-r--r--tests/twisted/text/test-text.py3
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'