diff options
author | Jonny Lamb <jonny.lamb@collabora.co.uk> | 2011-04-20 15:50:01 +0100 |
---|---|---|
committer | Jonny Lamb <jonny.lamb@collabora.co.uk> | 2011-04-20 15:50:37 +0100 |
commit | 2ce6227468c403cc6dbbfb21d2dd0524d3cf9256 (patch) | |
tree | f77120baba032b0d66c4fffb75c42496e218b837 | |
parent | f712a7db97f646fbde443bad9f5246092a7c510e (diff) |
caps_helper: copy newer version from gabble and everything it depends on
We need data form support.
Signed-off-by: Jonny Lamb <jonny.lamb@collabora.co.uk>
-rw-r--r-- | tests/twisted/caps_helper.py | 337 | ||||
-rw-r--r-- | tests/twisted/ns.py | 66 | ||||
-rw-r--r-- | tests/twisted/salut/status.py | 2 | ||||
-rw-r--r-- | tests/twisted/saluttest.py | 93 |
4 files changed, 469 insertions, 29 deletions
diff --git a/tests/twisted/caps_helper.py b/tests/twisted/caps_helper.py index 76faadb..d18d109 100644 --- a/tests/twisted/caps_helper.py +++ b/tests/twisted/caps_helper.py @@ -1,42 +1,335 @@ +# vim: set fileencoding=utf-8 : import hashlib import base64 +import dbus -from avahitest import txt_get_key +from twisted.words.xish import domish, xpath +from saluttest import make_result_iq, make_presence, elem_iq, elem +from salutservicetest import ( + EventPattern, + assertEquals, assertContains, assertDoesNotContain, assertLength, + ) + +import config import ns +import salutconstants as cs + +# The caps we always have, regardless of any clients' caps +FIXED_CAPS = [ + ns.JINGLE, + ns.JINGLE_015, + ns.GOOGLE_FEAT_SESSION, + ns.JINGLE_TRANSPORT_RAWUDP, + ns.NICK, + ns.NICK + '+notify', + ns.CHAT_STATES, + ns.SI, + ns.IBB, + ns.BYTESTREAMS, + ] + +JINGLE_CAPS = [ + # Additional Jingle transports + ns.JINGLE_TRANSPORT_ICEUDP, + ns.GOOGLE_P2P, + # Jingle content types + ns.GOOGLE_FEAT_VOICE, + ns.GOOGLE_FEAT_VIDEO, + ns.JINGLE_015_AUDIO, + ns.JINGLE_015_VIDEO, + ns.JINGLE_RTP, + ns.JINGLE_RTP_AUDIO, + ns.JINGLE_RTP_VIDEO, + ] + +VARIABLE_CAPS = ( + JINGLE_CAPS + + [ + ns.FILE_TRANSFER, + + # FIXME: currently we always advertise these, but in future we should + # only advertise them if >= 1 client supports them: + # ns.TUBES, + + # there is an unlimited set of these; only the ones actually relevant to + # the tests so far are shown here + ns.TUBES + '/stream#x-abiword', + ns.TUBES + '/stream#daap', + ns.TUBES + '/stream#http', + ns.TUBES + '/dbus#com.example.Go', + ns.TUBES + '/dbus#com.example.Xiangqi', + ]) + +def check_caps(namespaces, desired): + """Assert that all the FIXED_CAPS are supported, and of the VARIABLE_CAPS, + every capability in desired is supported, and every other capability is + not. + """ + for c in FIXED_CAPS: + assertContains(c, namespaces) + + for c in VARIABLE_CAPS: + if c in desired: + assertContains(c, namespaces) + else: + assertDoesNotContain(c, namespaces) + +text_fixed_properties = dbus.Dictionary({ + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT + }) +text_allowed_properties = dbus.Array([cs.TARGET_HANDLE]) + +stream_tube_fixed_properties = dbus.Dictionary({ + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_STREAM_TUBE + }) +stream_tube_allowed_properties = dbus.Array([cs.TARGET_HANDLE, + cs.TARGET_ID, cs.STREAM_TUBE_SERVICE]) + +dbus_tube_fixed_properties = dbus.Dictionary({ + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_DBUS_TUBE + }) +dbus_tube_allowed_properties = dbus.Array([cs.TARGET_HANDLE, + cs.TARGET_ID, cs.DBUS_TUBE_SERVICE_NAME]) + +ft_fixed_properties = dbus.Dictionary({ + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_FILE_TRANSFER, + }) +ft_allowed_properties = dbus.Array([ + cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentHashType', + cs.TARGET_HANDLE, + cs.TARGET_ID, + cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentType', + cs.CHANNEL_TYPE_FILE_TRANSFER + '.Filename', + cs.CHANNEL_TYPE_FILE_TRANSFER + '.Size', + cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentHash', + cs.CHANNEL_TYPE_FILE_TRANSFER + '.Description', + cs.CHANNEL_TYPE_FILE_TRANSFER + '.Date', + cs.FT_URI]) + +fake_client_dataforms = { + 'urn:xmpp:dataforms:softwareinfo': + {'software': ['A Fake Client with Twisted'], + 'software_version': ['5.11.2-svn-20080512'], + 'os': ['Debian GNU/Linux unstable (sid) unstable sid'], + 'os_version': ['2.6.24-1-amd64'], + }, +} def compute_caps_hash(identities, features, dataforms): - S = '' + """ + Accepts a list of slash-separated identities, a list of feature namespaces, + and a map from FORM_TYPE to (map from field name to values), returns the + verification string as defined by + <http://xmpp.org/extensions/xep-0115.html#ver>. + """ + components = [] for identity in sorted(identities): - S += '%s<' % identity + if len(identity.split('/')) != 4: + raise ValueError( + "expecting identities of the form " + + "'category/type/lang/client': got " + repr(identity)) + + components.append(identity) for feature in sorted(features): - S += '%s<' % feature + components.append(feature) + + for form_type in sorted(dataforms.keys()): + components.append(form_type) - # FIXME: support dataforms + for var in sorted(dataforms[form_type].keys()): + components.append(var) + + for value in sorted(dataforms[form_type][var]): + components.append(value) + + components.append('') m = hashlib.sha1() - m.update(S) + S = u'<'.join(components) + m.update(S.encode('utf-8')) return base64.b64encode(m.digest()) -def check_caps(txt, ver): - for (key, val) in { "1st": "test", - "last": "suite", - "status": "avail", - "txtvers": "1" }.iteritems(): - v = txt_get_key(txt, key) - assert v == val, (key, val, v) +def make_caps_disco_reply(stream, req, identities, features, dataforms={}): + iq = make_result_iq(stream, req) + query = iq.firstChildElement() + + for identity in identities: + category, type_, lang, name = identity.split('/') + el = query.addElement('identity') + el['category'] = category + el['type'] = type_ + el['name'] = name + + for f in features: + el = domish.Element((None, 'feature')) + el['var'] = f + query.addChild(el) + + for type, fields in dataforms.iteritems(): + x = query.addElement((ns.X_DATA, 'x')) + x['type'] = 'result' + + field = x.addElement('field') + field['var'] = 'FORM_TYPE' + field['type'] = 'hidden' + field.addElement('value', content=type) + + for var, values in fields.iteritems(): + field = x.addElement('field') + field['var'] = var + + for value in values: + field.addElement('value', content=value) + + return iq - assert txt_get_key(txt, "hash") == "sha-1" - assert txt_get_key(txt, "node") == ns.TELEPATHY_CAPS +def receive_presence_and_ask_caps(q, stream, expect_dbus=True): + # receive presence stanza + if expect_dbus: + presence, event_dbus = q.expect_many( + EventPattern('stream-presence'), + EventPattern('dbus-signal', signal='ContactCapabilitiesChanged') + ) + assertLength(1, event_dbus.args) + signaled_caps = event_dbus.args[0] + else: + presence = q.expect('stream-presence') + signaled_caps = None - v = txt_get_key(txt, "ver") - assert v == ver, (v, ver) + return disco_caps(q, stream, presence) + (signaled_caps,) + +def disco_caps(q, stream, presence): + c_nodes = xpath.queryForNodes('/presence/c', presence.stanza) + assert c_nodes is not None + assertLength(1, c_nodes) + hash = c_nodes[0].attributes['hash'] + ver = c_nodes[0].attributes['ver'] + node = c_nodes[0].attributes['node'] + assertEquals('sha-1', hash) + + # ask caps + request = \ + elem_iq(stream, 'get', from_='fake_contact@jabber.org/resource')( + elem(ns.DISCO_INFO, 'query', node=(node + '#' + ver)) + ) + stream.send(request) + + # receive caps + event = q.expect('stream-iq', query_ns=ns.DISCO_INFO, iq_id=request['id']) + + # Check that Gabble's announcing the identity we think it should be. + identity_nodes = xpath.queryForNodes('/iq/query/identity', event.stanza) + assertLength(1, identity_nodes) + identity_node = identity_nodes[0] + + assertEquals('client', identity_node['category']) + assertEquals(config.CLIENT_TYPE, identity_node['type']) + assertEquals(config.PACKAGE_STRING, identity_node['name']) + assertDoesNotContain('xml:lang', identity_node.attributes) + + identity = 'client/%s//%s' % (config.CLIENT_TYPE, config.PACKAGE_STRING) + + features = [] + for feature in xpath.queryForNodes('/iq/query/feature', event.stanza): + features.append(feature['var']) + + # Check if the hash matches the announced capabilities + assertEquals(compute_caps_hash([identity], features, {}), ver) + + return (event, features) + +def caps_contain(event, cap): + node = xpath.queryForNodes('/iq/query/feature[@var="%s"]' + % cap, + event.stanza) + if node is None: + return False + if len(node) != 1: + return False + var = node[0].attributes['var'] + if var is None: + return False + return var == cap + +def presence_and_disco(q, conn, stream, contact, disco, + client, caps, + features, identities=[], dataforms={}, + initial=True, show=None): + h = send_presence(q, conn, stream, contact, caps, initial=initial, + show=show) + + if disco: + stanza = expect_disco(q, contact, client, caps) + send_disco_reply(stream, stanza, identities, features, dataforms) + + return h + +def send_presence(q, conn, stream, contact, caps, initial=True, show=None): + h = conn.RequestHandles(cs.HT_CONTACT, [contact])[0] + + if initial: + stream.send(make_presence(contact, status='hello')) + + q.expect_many( + EventPattern('dbus-signal', signal='PresenceUpdate', + args=[{h: + (0L, {u'available': {'message': 'hello'}})}]), + EventPattern('dbus-signal', signal='PresencesChanged', + args=[{h: + (2, u'available', 'hello')}])) + + # no special capabilities + assertEquals([(h, cs.CHANNEL_TYPE_TEXT, 3, 0)], + conn.Capabilities.GetCapabilities([h])) + + # send updated presence with caps info + stream.send(make_presence(contact, show=show, status='hello', caps=caps)) + + return h + +def expect_disco(q, contact, client, caps): + # Gabble looks up our capabilities + event = q.expect('stream-iq', to=contact, query_ns=ns.DISCO_INFO) + assertEquals(client + '#' + caps['ver'], event.query['node']) + + return event.stanza + +def send_disco_reply(stream, stanza, identities, features, dataforms={}): + stream.send( + make_caps_disco_reply(stream, stanza, identities, features, dataforms)) if __name__ == '__main__': # example from XEP-0115 - assert compute_caps_hash(['client/pc//Exodus 0.9.1'], - ["http://jabber.org/protocol/disco#info", - "http://jabber.org/protocol/disco#items", - "http://jabber.org/protocol/muc", "http://jabber.org/protocol/caps"], - []) == 'QgayPKawpkPSDYmwT/WM94uAlu0=' + assertEquals('QgayPKawpkPSDYmwT/WM94uAlu0=', + compute_caps_hash(['client/pc//Exodus 0.9.1'], + ["http://jabber.org/protocol/disco#info", + "http://jabber.org/protocol/disco#items", + "http://jabber.org/protocol/muc", + "http://jabber.org/protocol/caps"], + {})) + + # another example from XEP-0115 + identities = [u'client/pc/en/Psi 0.11', u'client/pc/el/Ψ 0.11'] + features = [ + u'http://jabber.org/protocol/caps', + u'http://jabber.org/protocol/disco#info', + u'http://jabber.org/protocol/disco#items', + u'http://jabber.org/protocol/muc', + ] + dataforms = { + u'urn:xmpp:dataforms:softwareinfo': + { u'ip_version': [u'ipv4', u'ipv6'], + u'os': [u'Mac'], + u'os_version': [u'10.5.1'], + u'software': [u'Psi'], + u'software_version': [u'0.11'], + }, + } + assertEquals('q07IKJEyjvHSyhy//CH0CxmKi8w=', + compute_caps_hash(identities, features, dataforms)) diff --git a/tests/twisted/ns.py b/tests/twisted/ns.py index 07a78ca..fe9f6e2 100644 --- a/tests/twisted/ns.py +++ b/tests/twisted/ns.py @@ -1,11 +1,46 @@ AMP = "http://jabber.org/protocol/amp" +BYTESTREAMS = 'http://jabber.org/protocol/bytestreams' +CHAT_STATES = 'http://jabber.org/protocol/chatstates' +CAPS = "http://jabber.org/protocol/caps" DISCO_INFO = "http://jabber.org/protocol/disco#info" DISCO_ITEMS = "http://jabber.org/protocol/disco#items" -IBB = "http://jabber.org/protocol/ibb" -IQ_OOB = "jabber:iq:oob" +FEATURE_NEG = 'http://jabber.org/protocol/feature-neg' +FILE_TRANSFER = 'http://jabber.org/protocol/si/profile/file-transfer' +GEOLOC = 'http://jabber.org/protocol/geoloc' +GOOGLE_FEAT_SESSION = 'http://www.google.com/xmpp/protocol/session' +GOOGLE_FEAT_SHARE = 'http://google.com/xmpp/protocol/share/v1' +GOOGLE_FEAT_VOICE = 'http://www.google.com/xmpp/protocol/voice/v1' +GOOGLE_FEAT_VIDEO = 'http://www.google.com/xmpp/protocol/video/v1' +GOOGLE_JINGLE_INFO = 'google:jingleinfo' +GOOGLE_P2P = "http://www.google.com/transport/p2p" +GOOGLE_QUEUE = 'google:queue' +GOOGLE_ROSTER = 'google:roster' +GOOGLE_SESSION = "http://www.google.com/session" +GOOGLE_SESSION_SHARE = "http://www.google.com/session/share" +GOOGLE_SESSION_PHONE = "http://www.google.com/session/phone" +GOOGLE_SESSION_VIDEO = "http://www.google.com/session/video" +GOOGLE_MAIL_NOTIFY = "google:mail:notify" +IBB = 'http://jabber.org/protocol/ibb' +JINGLE_015 = "http://jabber.org/protocol/jingle" +JINGLE_015_AUDIO = "http://jabber.org/protocol/jingle/description/audio" +JINGLE_015_VIDEO = "http://jabber.org/protocol/jingle/description/video" +JINGLE = "urn:xmpp:jingle:1" +JINGLE_RTP = "urn:xmpp:jingle:apps:rtp:1" +JINGLE_RTP_AUDIO = "urn:xmpp:jingle:apps:rtp:audio" +JINGLE_RTP_VIDEO = "urn:xmpp:jingle:apps:rtp:video" +JINGLE_RTP_ERRORS = "urn:xmpp:jingle:apps:rtp:errors:1" +JINGLE_RTP_INFO_1 = "urn:xmpp:jingle:apps:rtp:info:1" +JINGLE_TRANSPORT_ICEUDP = "urn:xmpp:jingle:transports:ice-udp:1" +JINGLE_TRANSPORT_RAWUDP = "urn:xmpp:jingle:transports:raw-udp:1" MUC = 'http://jabber.org/protocol/muc' +MUC_BYTESTREAM = 'http://telepathy.freedesktop.org/xmpp/protocol/muc-bytestream' MUC_OWNER = '%s#owner' % MUC MUC_USER = '%s#user' % MUC +NICK = "http://jabber.org/protocol/nick" +NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl' +NS_XMPP_BIND = 'urn:ietf:params:xml:ns:xmpp-bind' +NS_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls' +NS_XMPP_SESSION = 'urn:ietf:params:xml:ns:xmpp-session' OLPC_ACTIVITIES = "http://laptop.org/xmpp/activities" OLPC_ACTIVITIES_NOTIFY = "%s+notify" % OLPC_ACTIVITIES OLPC_ACTIVITY = "http://laptop.org/xmpp/activity" @@ -17,8 +52,27 @@ OLPC_BUDDY_PROPS_NOTIFY = "%s+notify" % OLPC_BUDDY_PROPS OLPC_CURRENT_ACTIVITY = "http://laptop.org/xmpp/current-activity" OLPC_CURRENT_ACTIVITY_NOTIFY = "%s+notify" % OLPC_CURRENT_ACTIVITY PUBSUB = "http://jabber.org/protocol/pubsub" -SI = "http://jabber.org/protocol/si" +PUBSUB_EVENT = "%s#event" % PUBSUB +REGISTER = "jabber:iq:register" +ROSTER = "jabber:iq:roster" +SEARCH = 'jabber:iq:search' +SI = 'http://jabber.org/protocol/si' +SI_MULTIPLE = 'http://telepathy.freedesktop.org/xmpp/si-multiple' STANZA = "urn:ietf:params:xml:ns:xmpp-stanzas" -TELEPATHY_CAPS = 'http://telepathy.freedesktop.org/caps' -TUBES = "http://telepathy.freedesktop.org/xmpp/tubes" -X_OOB = "jabber:x:oob" +STREAMS = "urn:ietf:params:xml:ns:xmpp-streams" +TEMPPRES = "urn:xmpp:temppres:0" +TUBES = 'http://telepathy.freedesktop.org/xmpp/tubes' +MUJI = 'http://telepathy.freedesktop.org/xmpp/muji' +VCARD_TEMP = 'vcard-temp' +VCARD_TEMP_UPDATE = 'vcard-temp:x:update' +X_DATA = 'jabber:x:data' +X_DELAY = 'jabber:x:delay' +XML = 'http://www.w3.org/XML/1998/namespace' +X_OOB = 'jabber:x:oob' +IQ_OOB = 'jabber:iq:oob' +GABBLE_CAPS="http://telepathy.freedesktop.org/caps" +PRESENCE_INVISIBLE = 'presence-invisible' +PRIVACY = 'jabber:iq:privacy' +INVISIBLE = 'urn:xmpp:invisible:0' +GOOGLE_SHARED_STATUS = 'google:shared-status' +VERSION = 'jabber:iq:version' diff --git a/tests/twisted/salut/status.py b/tests/twisted/salut/status.py index 6218d28..46e634b 100644 --- a/tests/twisted/salut/status.py +++ b/tests/twisted/salut/status.py @@ -63,7 +63,7 @@ def test(q, bus, conn): # immediately. # announce a contact with the right caps - ver = compute_caps_hash([], [ycs.CAPABILITIES_PREFIX + CAP_NAME + '+notify'], []) + ver = compute_caps_hash([], [ycs.CAPABILITIES_PREFIX + CAP_NAME + '+notify'], {}) txt_record = { "txtvers": "1", "status": "avail", "node": CLIENT_NAME, "ver": ver, "hash": "sha-1"} contact_name = "test-status@" + get_host_name() diff --git a/tests/twisted/saluttest.py b/tests/twisted/saluttest.py index 7de7039..0193ab7 100644 --- a/tests/twisted/saluttest.py +++ b/tests/twisted/saluttest.py @@ -208,3 +208,96 @@ def wait_for_contact_in_publish(q, bus, conn, contact_name): handle = h return handle + +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, 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 |