diff options
author | Jonny Lamb <jonny.lamb@collabora.co.uk> | 2011-09-30 10:40:28 +0100 |
---|---|---|
committer | Jonny Lamb <jonny.lamb@collabora.co.uk> | 2011-09-30 10:40:28 +0100 |
commit | 862aee50a0b80054ff2b7d3d4030f37d3646fff3 (patch) | |
tree | 03b9f84888d52b796bcc7ffb6f45fe0306576590 /tests | |
parent | 2163dc3ca3569401b865fc97930cda4e8d378cc1 (diff) | |
parent | 512576d656e37c88cd4f9fc9927ed266a8816202 (diff) |
Merge branch 'gabble'
Diffstat (limited to 'tests')
-rw-r--r-- | tests/twisted/Makefile.am | 21 | ||||
-rwxr-xr-x | tests/twisted/gabble/hct.py | 185 | ||||
-rw-r--r-- | tests/twisted/gabble/message.py | 390 | ||||
-rw-r--r-- | tests/twisted/gabble/service.py | 307 | ||||
-rwxr-xr-x | tests/twisted/gabble/sidecar.py | 35 | ||||
-rw-r--r-- | tests/twisted/gabble/slow-service.py | 137 | ||||
-rw-r--r-- | tests/twisted/gabble/status.py | 286 | ||||
-rw-r--r-- | tests/twisted/gabblecaps_helper.py | 356 | ||||
-rw-r--r-- | tests/twisted/gabbleconstants.py | 458 | ||||
-rw-r--r-- | tests/twisted/gabbleservicetest.py | 640 | ||||
-rw-r--r-- | tests/twisted/gabbletest.py | 824 | ||||
-rw-r--r-- | tests/twisted/salut/slow-service.py | 202 | ||||
-rw-r--r-- | tests/twisted/saluttest.py | 2 | ||||
-rw-r--r-- | tests/twisted/tools/Makefile.am | 10 | ||||
-rw-r--r-- | tests/twisted/tools/gabble-exec-with-log.sh.in | 40 | ||||
-rw-r--r-- | tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service.in | 3 | ||||
-rw-r--r-- | tests/twisted/yconstants.py | 1 |
17 files changed, 3890 insertions, 7 deletions
diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am index 78175e5..1b26802 100644 --- a/tests/twisted/Makefile.am +++ b/tests/twisted/Makefile.am @@ -9,7 +9,15 @@ TWISTED_BASIC_TESTS += \ salut/message.py \ salut/status.py \ salut/service.py \ - salut/hct.py + salut/hct.py \ + salut/slow-service.py \ + gabble/sidecar.py \ + gabble/message.py \ + gabble/status.py \ + gabble/service.py \ + gabble/hct.py \ + gabble/slow-service.py + endif config.py: Makefile @@ -46,7 +54,9 @@ BASIC_TESTS_ENVIRONMENT = \ XDG_CACHE_HOME=@abs_top_builddir@/tests/twisted/tmp-$(TMPSUFFIX) \ G_DEBUG=fatal_criticals \ SALUT_PLUGIN_DIR=@abs_top_builddir@/salut/.libs \ - SALUT_TEST_BACKTRACE=1 + SALUT_TEST_BACKTRACE=1 \ + GABBLE_PLUGIN_DIR=@abs_top_builddir@/gabble/.libs \ + GABBLE_TEST_BACKTRACE=1 WITH_SESSION_BUS = \ sh $(srcdir)/tools/with-session-bus.sh \ @@ -63,6 +73,7 @@ check-twisted: rm -f tools/core rm -f tools/missioncontrol-*.log rm -f tools/salut-testing.log + rm -f tools/gabble-testing.log mkdir tmp-$$$$ && { \ $(MAKE) check-combined TMPSUFFIX=$$$$; \ e=$$?; \ @@ -143,7 +154,11 @@ EXTRA_DIST = \ avahitest.py \ xmppstream.py \ ipv6.py \ - caps_helper.py + caps_helper.py \ + gabbleservicetest.py \ + gabbletest.py \ + gabbleconstants.py \ + gabblecaps_helper.py CLEANFILES = \ accounts/accounts.cfg \ diff --git a/tests/twisted/gabble/hct.py b/tests/twisted/gabble/hct.py new file mode 100755 index 0000000..714afcc --- /dev/null +++ b/tests/twisted/gabble/hct.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +import dbus + +from gabbleservicetest import call_async, EventPattern, assertEquals, \ + assertLength, assertContains, sync_dbus, ProxyWrapper +from gabbletest import exec_test, elem_iq, elem +from gabblecaps_helper import presence_and_disco, receive_presence_and_ask_caps, \ + disco_caps + +import gabbleconstants as cs +import ns +import yconstants as ycs + +client = 'http://telepathy.im/fake' +features = [ + ns.JINGLE_015, + ns.JINGLE_015_AUDIO, + ns.JINGLE_015_VIDEO, + ns.GOOGLE_P2P, + ycs.SERVICE_NS + '#the.target.service' + ] +identity = ['client/pc/en/Lolclient 0.L0L'] + +def test(q, bus, conn, stream): + bare_jid = "test-hct@example.com" + full_jid = bare_jid + "/LikeLava" + + call_async(q, conn.Future, 'EnsureSidecar', ycs.STATUS_IFACE) + conn.Connect() + + q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) + + e = q.expect('dbus-return', method='EnsureSidecar') + path, props = e.value + assertEquals({}, props) + + status = ProxyWrapper(bus.get_object(conn.bus_name, path), + ycs.STATUS_IFACE, {}) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({}, discovered) + + self_handle = conn.GetSelfHandle() + self_handle_name = conn.InspectHandles(cs.HT_CONTACT, [self_handle])[0] + + caps = {'ver': '0.1', 'node': client} + presence_and_disco(q, conn, stream, full_jid, True, client, caps, + features, identity, {}, True, None) + + # now update the caps + conn.ContactCapabilities.UpdateCapabilities([ + ('well.gnome.name', [], + ['com.meego.xpmn.ytstenut.Channel/uid/org.gnome.Banshee', + 'com.meego.xpmn.ytstenut.Channel/type/application', + 'com.meego.xpmn.ytstenut.Channel/name/en_GB/Banshee Media Player', + 'com.meego.xpmn.ytstenut.Channel/name/fr/Banshee Lecteur de Musique', + 'com.meego.xpmn.ytstenut.Channel/caps/urn:ytstenut:capabilities:yts-caps-audio', + 'com.meego.xpmn.ytstenut.Channel/caps/urn:ytstenut:data:jingle:rtp'])]) + + + _, e = q.expect_many(EventPattern('dbus-signal', signal='ServiceAdded'), + EventPattern('stream-presence')) + + e, _, _ = disco_caps(q, stream, e) + + iq = e.stanza + query = iq.children[0] + + x = None + for child in query.children: + if child.name == 'x' and child.uri == ns.X_DATA: + # we should only have one child + assert x is None + x = child + # don't break here as we can waste time to make sure x + # isn't assigned twice + + assert x is not None + + for field in x.children: + if field['var'] == 'FORM_TYPE': + assertEquals('hidden', field['type']) + assertEquals('urn:ytstenut:capabilities#org.gnome.Banshee', + field.children[0].children[0]) + elif field['var'] == 'type': + assertEquals('application', field.children[0].children[0]) + elif field['var'] == 'name': + names = [a.children[0] for a in field.children] + assertLength(2, names) + assertContains('en_GB/Banshee Media Player', names) + assertContains('fr/Banshee Lecteur de Musique', names) + elif field['var'] == 'capabilities': + caps = [a.children[0] for a in field.children] + assertLength(2, caps) + assertContains('urn:ytstenut:capabilities:yts-caps-audio', caps) + assertContains('urn:ytstenut:data:jingle:rtp', caps) + else: + assert False + + # now add another service + forbidden = [EventPattern('dbus-signal', signal='ServiceRemoved')] + q.forbid_events(forbidden) + + conn.ContactCapabilities.UpdateCapabilities([ + ('another.nice.gname', [], + ['com.meego.xpmn.ytstenut.Channel/uid/org.gnome.Eog', + 'com.meego.xpmn.ytstenut.Channel/type/application', + 'com.meego.xpmn.ytstenut.Channel/name/en_GB/Eye Of Gnome', + 'com.meego.xpmn.ytstenut.Channel/name/it/Occhio Di uno Gnomo', + 'com.meego.xpmn.ytstenut.Channel/caps/urn:ytstenut:capabilities:yts-picz'])]) + + e = q.expect('dbus-signal', signal='ServiceAdded') + + sync_dbus(bus, q, conn) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({'test@localhost': + {'org.gnome.Banshee': ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp']), + 'org.gnome.Eog': ('application', + {'en_GB': 'Eye Of Gnome', + 'it': 'Occhio Di uno Gnomo'}, + ['urn:ytstenut:capabilities:yts-picz'])}}, + discovered) + + q.unforbid_events(forbidden) + + forbidden = [EventPattern('dbus-signal', signal='ServiceRemoved', + args=[self_handle_name, 'org.gnome.Eog'])] + q.forbid_events(forbidden) + + conn.ContactCapabilities.UpdateCapabilities([ + ('well.gnome.name', [], [])]) + + e = q.expect('dbus-signal', signal='ServiceRemoved', + args=[self_handle_name, 'org.gnome.Banshee']) + + sync_dbus(bus, q, conn) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({'test@localhost': + {'org.gnome.Eog': ('application', + {'en_GB': 'Eye Of Gnome', + 'it': 'Occhio Di uno Gnomo'}, + ['urn:ytstenut:capabilities:yts-picz'])}}, + discovered) + + q.unforbid_events(forbidden) + + conn.ContactCapabilities.UpdateCapabilities([ + ('another.nice.gname', [], [])]) + + e = q.expect('dbus-signal', signal='ServiceRemoved', + args=[self_handle_name, 'org.gnome.Eog']) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({}, discovered) + +if __name__ == '__main__': + exec_test(test, do_connect=False) diff --git a/tests/twisted/gabble/message.py b/tests/twisted/gabble/message.py new file mode 100644 index 0000000..43e941f --- /dev/null +++ b/tests/twisted/gabble/message.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +from gabbleservicetest import call_async, EventPattern, assertEquals, ProxyWrapper +from gabbletest import exec_test, make_result_iq, sync_stream +from gabblecaps_helper import presence_and_disco + +from twisted.words.protocols.jabber.client import IQ +from twisted.words.xish.domish import Element + +import gabbleconstants as cs +import yconstants as ycs +import ns + +client = 'http://telepathy.im/fake' +caps = {'ver': '0.1', 'node': client} +features = [ + ns.JINGLE_015, + ns.JINGLE_015_AUDIO, + ns.JINGLE_015_VIDEO, + ns.GOOGLE_P2P, + ycs.SERVICE_NS + '#the.target.service' + ] +identity = ['client/pc/en/Lolclient 0.L0L'] + +def wrap_channel(bus, conn, path): + return ProxyWrapper(bus.get_object(conn.bus_name, path), + ycs.CHANNEL_IFACE, {}) + +def setup_tests(q, bus, conn, stream, announce=False): + bare_jid = "test-yst-message@example.com" + full_jid = bare_jid + "/HotHotResource" + + if announce: + presence_and_disco(q, conn, stream, full_jid, + True, client, caps, + features, identity, {}, + True, None) + + sync_stream(q, stream) + + handle = conn.RequestHandles(cs.HT_CONTACT, [full_jid])[0] + + return handle, bare_jid, full_jid + +def setup_outgoing_tests(q, bus, conn, stream, announce=True): + handle, _, _ = setup_tests(q, bus, conn, stream, announce) + + # okay we got our contact, let's go + request_props = { + cs.CHANNEL_TYPE: ycs.CHANNEL_IFACE, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_HANDLE: handle, + ycs.REQUEST_TYPE: ycs.REQUEST_TYPE_GET, + ycs.REQUEST_ATTRIBUTES: {'hi': 'mom'}, + ycs.TARGET_SERVICE: 'the.target.service', + ycs.INITIATOR_SERVICE: 'the.initiator.service' + } + + call_async(q, conn.Requests, 'CreateChannel', request_props) + + e, _ = q.expect_many(EventPattern('dbus-return', method='CreateChannel'), + EventPattern('dbus-signal', signal='NewChannels')) + path, props = e.value + + for k, v in request_props.items(): + assertEquals(v, props[k]) + + # finally we have our channel + chan = wrap_channel(bus, conn, path) + + # let's check we can't call Fail()/Reply() + call_async(q, chan, 'Fail', ycs.ERROR_TYPE_CANCEL, 'lol', 'whut', 'pear') + q.expect('dbus-error', method='Fail') + + call_async(q, chan, 'Reply', {'lol':'whut'}, '') + q.expect('dbus-error', method='Reply') + + # okay enough, let's move on. + call_async(q, chan, 'Request') + + e, _ = q.expect_many(EventPattern('stream-iq'), + EventPattern('dbus-return', method='Request')) + + assertEquals('get', e.iq_type) + assertEquals('message', e.query_name) + assertEquals('urn:ytstenut:message', e.query_ns) + + # we shouldn't be able to call this again + call_async(q, chan, 'Request') + q.expect('dbus-error', method='Request') + + return path, e.stanza + +def outgoing_reply(q, bus, conn, stream): + path, stanza = setup_outgoing_tests(q, bus, conn, stream) + + # reply with nothing + reply = make_result_iq(stream, stanza) + stream.send(reply) + + e = q.expect('dbus-signal', signal='Replied', path=path) + args, xml = e.args + assertEquals({}, args) + assertEquals('<?xml version="1.0" encoding="UTF-8"?>\n' \ + + '<message xmlns="urn:ytstenut:message"/>\n', xml) + +def outgoing_fail(q, bus, conn, stream): + path, stanza = setup_outgoing_tests(q, bus, conn, stream) + + # construct a nice error reply + reply = IQ(None, 'error') + reply['id'] = stanza['id'] + reply['from'] = stanza['to'] + error = reply.addElement('error') + error['type'] = 'cancel' + error['code'] = '409' + error.addElement((ns.STANZA, 'conflict')) + error.addElement((ycs.MESSAGE_NS, 'yodawg')) + text = error.addElement((ns.STANZA, 'text'), + content='imma let you finish') + + stream.send(reply) + + e = q.expect('dbus-signal', signal='Failed', path=path) + error_type, stanza_error_name, yst_error_name, text = e.args + assertEquals(ycs.ERROR_TYPE_CANCEL, error_type) + assertEquals('conflict', stanza_error_name) + assertEquals('yodawg', yst_error_name) + assertEquals('imma let you finish', text) + +def bad_requests(q, bus, conn, stream): + handle, _, _ = setup_tests(q, bus, conn, stream) + + props = { + cs.CHANNEL_TYPE: ycs.CHANNEL_IFACE, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + } + + def ensure_error(extra={}): + copy = props.copy() + copy.update(extra) + + call_async(q, conn.Requests, 'CreateChannel', copy) + q.expect('dbus-error', method='CreateChannel') + + # bad handle + ensure_error({cs.TARGET_HANDLE: 42}) + + # offline + ensure_error({cs.TARGET_ID: 'lolbags@dingdong'}) + + props.update({cs.TARGET_HANDLE: handle}) + + # RequestType + ensure_error() + ensure_error({ycs.REQUEST_TYPE: 99}) + props.update({ycs.REQUEST_TYPE: ycs.REQUEST_TYPE_GET}) + + # TargetService + ensure_error() + ensure_error({ycs.TARGET_SERVICE: 'lol/bags/what\'s this?!!!!'}) + props.update({ycs.TARGET_SERVICE: 'the.target.service'}) + + # InitiatorService + ensure_error() + ensure_error({ycs.INITIATOR_SERVICE: 'lol/bags/what\'s this?!!!!'}) + props.update({ycs.INITIATOR_SERVICE: 'the.initiator.service'}) + + # RequestAttributes: a{ss}, not a{si} + ensure_error({ycs.REQUEST_ATTRIBUTES: {'lol': 2}}) + + # RequestBody + ensure_error({ycs.REQUEST_BODY: 'no way is this real XML'}) + +def setup_incoming_tests(q, bus, conn, stream): + handle, bare_jid, full_jid = setup_tests(q, bus, conn, stream) + + self_handle = conn.GetSelfHandle() + self_handle_name = conn.InspectHandles(cs.HT_CONTACT, [self_handle])[0] + + iq = IQ(None, 'get') + iq['id'] = 'le-loldongs' + iq['from'] = full_jid + iq['to'] = self_handle_name + msg = iq.addElement((ycs.MESSAGE_NS, 'message')) + msg['from-service'] = 'the.from.service' + msg['to-service'] = 'the.to.service' + msg['owl-companions'] = 'the pussy cat' + msg['destination'] = 'sea' + msg['seacraft'] = 'beautiful pea green boat' + + lol = msg.addElement((None, 'lol')) + lol['some'] = 'stuff' + lol['to'] = 'fill' + lol['the'] = 'time' + lol.addElement((None, 'look-into-my-eyes'), + content='and tell me how boring writing these tests is') + + stream.send(iq) + + e = q.expect('dbus-signal', signal='NewChannels', predicate=lambda e: + e.args[0][0][1][cs.CHANNEL_TYPE] == ycs.CHANNEL_IFACE) + path, props = e.args[0][0] + + assertEquals(handle, props[cs.INITIATOR_HANDLE]) + assertEquals(bare_jid, props[cs.INITIATOR_ID]) + assertEquals(False, props[cs.REQUESTED]) + assertEquals(handle, props[cs.TARGET_HANDLE]) + assertEquals(cs.HT_CONTACT, props[cs.TARGET_HANDLE_TYPE]) + assertEquals(bare_jid, props[cs.TARGET_ID]) + + assertEquals('the.from.service', props[ycs.INITIATOR_SERVICE]) + assertEquals('the.to.service', props[ycs.TARGET_SERVICE]) + assertEquals(ycs.REQUEST_TYPE_GET, props[ycs.REQUEST_TYPE]) + assertEquals({'destination': 'sea', + 'owl-companions': 'the pussy cat', + 'seacraft': 'beautiful pea green boat'}, + props[ycs.REQUEST_ATTRIBUTES]) + + assertEquals('<?xml version="1.0" encoding="UTF-8"?>\n' \ + '<message seacraft="beautiful pea green boat" ' \ + 'from-service="the.from.service" destination="sea" ' \ + 'owl-companions="the pussy cat" to-service="the.to.service" ' \ + 'xmlns="urn:ytstenut:message">' \ + '<lol to="fill" the="time" some="stuff">' \ + '<look-into-my-eyes>and tell me how boring ' \ + 'writing these tests is</look-into-my-eyes>' \ + '</lol></message>\n', props[ycs.REQUEST_BODY]) + + # finally we have our channel + chan = wrap_channel(bus, conn, path) + + # let's check we can't call Request() + call_async(q, chan, 'Request') + q.expect('dbus-error', method='Request') + + return chan, bare_jid, full_jid, self_handle_name + +def incoming_reply(q, bus, conn, stream): + chan, bare_jid, full_jid, self_handle_name = \ + setup_incoming_tests(q, bus, conn, stream) + + moar = Element((ycs.MESSAGE_NS, 'message')) + moar['ninety-nine-problems'] = 'but a sauvignon blanc aint one' + moar['also'] = 'my mum said hi' + trollface = moar.addElement('trollface', content='problem?') + + call_async(q, chan, 'Reply', + {'ninety-nine-problems': 'but a sauvignon blanc aint one', + 'also': 'my mum said hi'}, + moar.toXml()) + + _, e = q.expect_many(EventPattern('dbus-return', method='Reply'), + EventPattern('stream-message')) + + iq = e.stanza + assertEquals('le-loldongs', iq['id']) + assertEquals('result', iq['type']) + assertEquals(self_handle_name, iq['from']) + assertEquals(full_jid, iq['to']) + assertEquals(1, len(iq.children)) + + message = iq.children[0] + + assertEquals('message', message.name) + assertEquals(ycs.MESSAGE_NS, message.uri) + assertEquals('my mum said hi', message['also']) + assertEquals('but a sauvignon blanc aint one', message['ninety-nine-problems']) + assertEquals('the.from.service', message['to-service']) + assertEquals('the.to.service', message['from-service']) + assertEquals(1, len(message.children)) + + trollface = message.children[0] + + assertEquals('trollface', trollface.name) + assertEquals(1, len(trollface.children)) + + assertEquals('problem?', trollface.children[0]) + + # check we can't call anything any more + call_async(q, chan, 'Fail', ycs.ERROR_TYPE_CANCEL, 'lol', 'whut', 'pear') + q.expect('dbus-error', method='Fail') + + call_async(q, chan, 'Reply', {'lol':'whut'}, '') + q.expect('dbus-error', method='Reply') + + call_async(q, chan, 'Request') + q.expect('dbus-error', method='Request') + +def incoming_fail(q, bus, conn, stream): + chan, bare_jid, full_jid, self_handle_name = \ + setup_incoming_tests(q, bus, conn, stream) + + call_async(q, chan, 'Fail', + ycs.ERROR_TYPE_AUTH, 'auth', 'omgwtfbbq', + 'I most certainly dont feel like dancing') + + _, e = q.expect_many(EventPattern('dbus-return', method='Fail'), + EventPattern('stream-message')) + + iq = e.stanza + assertEquals('le-loldongs', iq['id']) + assertEquals('error', iq['type']) + assertEquals(self_handle_name, iq['from']) + assertEquals(full_jid, iq['to']) + assertEquals(2, len(iq.children)) + + def check_message(message): + assertEquals('message', message.name) + assertEquals(ycs.MESSAGE_NS, message.uri) + assertEquals('beautiful pea green boat', message['seacraft']) + assertEquals('sea', message['destination']) + assertEquals('the pussy cat', message['owl-companions']) + assertEquals('the.from.service', message['from-service']) + assertEquals('the.to.service', message['to-service']) + assertEquals(1, len(message.children)) + + lol = message.children[0] + + assertEquals('lol', lol.name) + assertEquals('fill', lol['to']) + assertEquals('time', lol['the']) + assertEquals('stuff', lol['some']) + assertEquals(1, len(lol.children)) + + look = lol.children[0] + + assertEquals('look-into-my-eyes', look.name) + assertEquals(1, len(look.children)) + assertEquals('and tell me how boring writing these tests is', look.children[0]) + + def check_error(error): + assertEquals('error', error.name) + assertEquals('auth', error['type']) + assertEquals(3, len(error.children)) + + for c in error.children: + if c.name == 'auth': + assertEquals(ns.STANZA, c.uri) + elif c.name == 'omgwtfbbq': + assertEquals(ycs.MESSAGE_NS, c.uri) + elif c.name == 'text': + assertEquals(ns.STANZA, c.uri) + assertEquals(1, len(c.children)) + assertEquals('I most certainly dont feel like dancing', + c.children[0]) + else: + raise + + for child in iq.children: + if child.name == 'message': + check_message(child) + elif child.name == 'error': + check_error(child) + else: + raise + + # check we can't call anything any more + call_async(q, chan, 'Fail', ycs.ERROR_TYPE_CANCEL, 'lol', 'whut', 'pear') + q.expect('dbus-error', method='Fail') + + call_async(q, chan, 'Reply', {'lol':'whut'}, '') + q.expect('dbus-error', method='Reply') + + call_async(q, chan, 'Request') + q.expect('dbus-error', method='Request') + +if __name__ == '__main__': + exec_test(outgoing_reply) + exec_test(outgoing_fail) + exec_test(bad_requests) + exec_test(incoming_reply) + exec_test(incoming_fail) diff --git a/tests/twisted/gabble/service.py b/tests/twisted/gabble/service.py new file mode 100644 index 0000000..6ee49f4 --- /dev/null +++ b/tests/twisted/gabble/service.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +import dbus + +from gabbleservicetest import call_async, EventPattern, assertEquals, \ + ProxyWrapper, assertNotEquals, assertSameSets +from gabbletest import exec_test, make_result_iq, sync_stream +import gabbleconstants as cs +import yconstants as ycs +from gabblecaps_helper import * + +from twisted.words.xish import xpath +from twisted.words.xish.domish import Element +from twisted.words.xish import domish + +CLIENT_NAME = 'il-cliente-del-futuro' + +client = 'http://telepathy.im/fake' +client_caps = {'ver': '0.1', 'node': client} +features = [ + ns.JINGLE_015, + ns.JINGLE_015_AUDIO, + ns.JINGLE_015_VIDEO, + ns.GOOGLE_P2P, + ] +identity = ['client/pc/en/Lolclient 0.L0L'] + +banshee = { + 'urn:ytstenut:capabilities#org.gnome.Banshee': + {'type': ['application'], + 'name': ['en_GB/Banshee Media Player', + 'fr/Banshee Lecteur de Musique'], + 'capabilities': ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'] + } +} + +evince = { + 'urn:ytstenut:capabilities#org.gnome.Evince': + {'type': ['application'], + 'name': ['en_GB/Evince Picture Viewer', + 'fr/Evince uh, ow do you say'], + 'capabilities': ['urn:ytstenut:capabilities:pics'], + } +} + +# TODO: move more of the common parts of this test into different +# functions to cut out the duplication! + +def test(q, bus, conn, stream): + call_async(q, conn.Future, 'EnsureSidecar', ycs.STATUS_IFACE) + + conn.Connect() + + # Now we're connected, the call we made earlier should return. + e = q.expect('dbus-return', method='EnsureSidecar') + path, props = e.value + assertEquals({}, props) + + status = ProxyWrapper(bus.get_object(conn.bus_name, path), + ycs.STATUS_IFACE, {}) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({}, discovered) + + # announce a contact with the right caps + bare_jid = "test-service@example.com" + full_jid = bare_jid + "/NeeNawNeeNawIAmAnAmbulance" + + contact_handle = conn.RequestHandles(cs.HT_CONTACT, [bare_jid])[0] + + client_caps['ver'] = compute_caps_hash(identity, features, banshee) + presence_and_disco(q, conn, stream, full_jid, True, client, client_caps, + features, identity, banshee, True, None) + + # this will be fired as text channel caps will be fired + _, e = q.expect_many(EventPattern('dbus-signal', signal='ContactCapabilitiesChanged', + predicate=lambda e: contact_handle in e.args[0]), + EventPattern('dbus-signal', signal='ServiceAdded')) + + contact_id, service_name, details = e.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Banshee', service_name) + + type, name_map, caps = details + assertEquals('application', type) + assertEquals({'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, name_map) + assertSameSets(['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'], caps) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'])}, + }, discovered) + + # add evince + tmp = banshee.copy() + tmp.update(evince) + client_caps['ver'] = compute_caps_hash(identity, features, tmp) + presence_and_disco(q, conn, stream, full_jid, True, client, client_caps, + features, identity, tmp, False) + + e = q.expect('dbus-signal', signal='ServiceAdded') + + contact_id, service_name, details = e.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Evince', service_name) + + type, name_map, caps = details + assertEquals('application', type) + assertEquals({'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, name_map) + assertSameSets(['urn:ytstenut:capabilities:pics'], caps) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp']), + 'org.gnome.Evince': + ('application', + {'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, + ['urn:ytstenut:capabilities:pics'])} + }, discovered) + + # remove evince + forbidden = [EventPattern('dbus-signal', signal='stream-iq')] + q.forbid_events(forbidden) + + client_caps['ver'] = compute_caps_hash(identity, features, banshee) + send_presence(q, conn, stream, full_jid, client_caps, initial=False) + + e = q.expect('dbus-signal', signal='ServiceRemoved') + + contact_id, service_name = e.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Evince', service_name) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'])}, + }, discovered) + + sync_stream(q, stream) + + q.unforbid_events(forbidden) + + # now just evince + client_caps['ver'] = compute_caps_hash(identity, features, evince) + presence_and_disco(q, conn, stream, full_jid, True, client, client_caps, + features, identity, evince, False) + + sa, sr = q.expect_many(EventPattern('dbus-signal', signal='ServiceAdded'), + EventPattern('dbus-signal', signal='ServiceRemoved')) + + contact_id, service_name, details = sa.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Evince', service_name) + + type, name_map, caps = details + assertEquals('application', type) + assertEquals({'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, name_map) + assertSameSets(['urn:ytstenut:capabilities:pics'], caps) + + contact_id, service_name = sr.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Banshee', service_name) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Evince': + ('application', + {'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, + ['urn:ytstenut:capabilities:pics'])} + }, discovered) + + # just banshee again + forbidden = [EventPattern('dbus-signal', signal='stream-iq')] + q.forbid_events(forbidden) + + client_caps['ver'] = compute_caps_hash(identity, features, banshee) + send_presence(q, conn, stream, full_jid, client_caps, initial=False) + + sr, sa = q.expect_many(EventPattern('dbus-signal', signal='ServiceRemoved'), + EventPattern('dbus-signal', signal='ServiceAdded')) + + contact_id, service_name = sr.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Evince', service_name) + + contact_id, service_name, details = sa.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Banshee', service_name) + + type, name_map, caps = details + assertEquals('application', type) + assertEquals({'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, name_map) + assertSameSets(['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'], caps) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'])} + }, discovered) + + sync_stream(q, stream) + + # both again + client_caps['ver'] = compute_caps_hash(identity, features, tmp) + send_presence(q, conn, stream, full_jid, client_caps, initial=False) + + sa = q.expect('dbus-signal', signal='ServiceAdded') + + contact_id, service_name, details = sa.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Evince', service_name) + + type, name_map, caps = details + assertEquals('application', type) + assertEquals({'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, name_map) + assertSameSets(['urn:ytstenut:capabilities:pics'], caps) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp']), + 'org.gnome.Evince': + ('application', + {'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, + ['urn:ytstenut:capabilities:pics'])} + }, discovered) + + sync_stream(q, stream) + + q.unforbid_events(forbidden) + + # and finally, nothing + client_caps['ver'] = compute_caps_hash(identity, features, {}) + presence_and_disco(q, conn, stream, full_jid, True, client, client_caps, + features, identity, {}, False) + + q.expect_many(EventPattern('dbus-signal', signal='ServiceRemoved', + args=[bare_jid, 'org.gnome.Banshee']), + EventPattern('dbus-signal', signal='ServiceRemoved', + args=[bare_jid, 'org.gnome.Evince'])) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({}, discovered) + + # super. + +if __name__ == '__main__': + exec_test(test, do_connect=False) diff --git a/tests/twisted/gabble/sidecar.py b/tests/twisted/gabble/sidecar.py new file mode 100755 index 0000000..92e16b7 --- /dev/null +++ b/tests/twisted/gabble/sidecar.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +from gabbleservicetest import call_async, EventPattern, assertEquals +from gabbletest import exec_test +from yconstants import STATUS_IFACE + +def test(q, bus, conn, stream): + call_async(q, conn.Future, 'EnsureSidecar', STATUS_IFACE) + + conn.Connect() + + # Now we're connected, the call we made earlier should return. + e = q.expect('dbus-return', method='EnsureSidecar') + path, props = e.value + assertEquals({}, props) + +if __name__ == '__main__': + exec_test(test, do_connect=False) diff --git a/tests/twisted/gabble/slow-service.py b/tests/twisted/gabble/slow-service.py new file mode 100644 index 0000000..d51cdd8 --- /dev/null +++ b/tests/twisted/gabble/slow-service.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +import dbus + +from gabbleservicetest import call_async, EventPattern, assertEquals, \ + ProxyWrapper, assertNotEquals, assertSameSets +from gabbletest import exec_test, make_result_iq, sync_stream +import gabbleconstants as cs +import yconstants as ycs +from gabblecaps_helper import * + +from twisted.words.xish import xpath +from twisted.words.xish.domish import Element +from twisted.words.xish import domish + +CLIENT_NAME = 'il-cliente-del-futuro' + +client = 'http://telepathy.im/fake' +client_caps = {'ver': '0.1', 'node': client} +features = [ + ns.JINGLE_015, + ns.JINGLE_015_AUDIO, + ns.JINGLE_015_VIDEO, + ns.GOOGLE_P2P, + ] +identity = ['client/pc/en/Lolclient 0.L0L'] + +banshee = { + 'urn:ytstenut:capabilities#org.gnome.Banshee': + {'type': ['application'], + 'name': ['en_GB/Banshee Media Player', + 'fr/Banshee Lecteur de Musique'], + 'capabilities': ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'] + } +} + +evince = { + 'urn:ytstenut:capabilities#org.gnome.Evince': + {'type': ['application'], + 'name': ['en_GB/Evince Picture Viewer', + 'fr/Evince uh, ow do you say'], + 'capabilities': ['urn:ytstenut:capabilities:pics'], + } +} + +def test(q, bus, conn, stream): + bare_jid = "test-service@example.com" + full_jid = bare_jid + "/NeeNawNeeNawIAmAnAmbulance" + + # we don't want these two signalled, ever. + forbidden = [EventPattern('dbus-signal', signal='ServiceAdded'), + EventPattern('dbus-signal', signal='ServiceRemoved')] + q.forbid_events(forbidden) + + conn.Connect() + + _, e = q.expect_many(EventPattern('dbus-signal', signal='StatusChanged', + args=[0, 1]), + EventPattern('stream-iq', query_ns=ns.ROSTER, + iq_type='get', query_name='query')) + + e.stanza['type'] = 'result' + + item = e.query.addElement('item') + item['jid'] = bare_jid + item['subscription'] = 'both' + + stream.send(e.stanza) + + q.expect('dbus-signal', signal='ContactListStateChanged', + args=[cs.CONTACT_LIST_STATE_SUCCESS]) + + # announce a contact with the right caps + contact_handle = conn.RequestHandles(cs.HT_CONTACT, [bare_jid])[0] + + client_caps['ver'] = compute_caps_hash(identity, features, banshee) + presence_and_disco(q, conn, stream, full_jid, True, client, client_caps, + features, identity, banshee, True, None) + + # this will be fired as text channel caps will be fired + q.expect('dbus-signal', signal='ContactCapabilitiesChanged', + predicate=lambda e: contact_handle in e.args[0]) + + # add evince + tmp = banshee.copy() + tmp.update(evince) + client_caps['ver'] = compute_caps_hash(identity, features, tmp) + presence_and_disco(q, conn, stream, full_jid, True, client, client_caps, + features, identity, tmp, False) + + sync_stream(q, stream) + + # now finally ensure the sidecar + path, props = conn.Future.EnsureSidecar(ycs.STATUS_IFACE) + assertEquals({}, props) + + status = ProxyWrapper(bus.get_object(conn.bus_name, path), + ycs.STATUS_IFACE, {}) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp']), + 'org.gnome.Evince': + ('application', + {'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, + ['urn:ytstenut:capabilities:pics'])} + }, discovered) + + # sweet. + +if __name__ == '__main__': + exec_test(test, do_connect=False) diff --git a/tests/twisted/gabble/status.py b/tests/twisted/gabble/status.py new file mode 100644 index 0000000..8917575 --- /dev/null +++ b/tests/twisted/gabble/status.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +import dbus + +from gabbleservicetest import call_async, EventPattern, assertEquals, \ + ProxyWrapper, assertNotEquals +from gabbletest import exec_test, make_result_iq, acknowledge_iq +import gabbleconstants as cs +import yconstants as ycs +from gabblecaps_helper import * + +from twisted.words.xish import xpath +from twisted.words.xish.domish import Element +from twisted.words.xish import domish + +CAP_NAME = 'urn:ytstenut:capabilities:h264-over-ants' +CLIENT_NAME = 'fake-client' + +client = 'http://telepathy.im/fake' +caps = {'ver': '0.1', 'node': client} +features = [ + ns.JINGLE_015, + ns.JINGLE_015_AUDIO, + ns.JINGLE_015_VIDEO, + ns.GOOGLE_P2P, + ycs.SERVICE_NS + '#the.target.service', + CAP_NAME + '+notify' + ] +identity = ['client/pc/en/Lolclient 0.L0L'] + +def check_pep_set(iq): + pubsub = iq.children[0] + publish = pubsub.children[0] + item = publish.children[0] + status_el = item.children[0] + + desc = None + if status_el.children: + desc = status_el.children[0] + + assertEquals('set', iq['type']) + assertEquals(1, len(iq.children)) + + assertEquals('pubsub', pubsub.name) + assertEquals(ns.PUBSUB, pubsub.uri) + assertEquals(1, len(pubsub.children)) + + assertEquals('publish', publish.name) + assertEquals(CAP_NAME, publish['node']) + assertEquals(1, len(publish.children)) + + assertEquals('item', item.name) + assertEquals(1, len(item.children)) + + assertEquals('status', status_el.name) + + if desc: + assertEquals(1, len(status_el.children)) + + assertEquals('description', desc.name) + assertEquals(1, len(desc.children)) + + return status_el, desc + +def send_back_pep_event(stream, status_el): + msg = Element((None, 'message')) + msg['type'] = 'headline' + msg['from'] = 'test@localhost' + msg['to'] = 'test@localhost/Resource' + msg['id'] = 'le-headline' + event = msg.addElement('event') + event['xmlns'] = ns.PUBSUB_EVENT + items = event.addElement('items') + items['node'] = CAP_NAME + item = items.addElement('item') + + # just steal this + item.addChild(status_el) + + # and go + stream.send(msg) + +def test(q, bus, conn, stream): + # we won't be using any data forms, so these two shouldn't ever be + # fired. + q.forbid_events([EventPattern('dbus-signal', signal='ServiceAdded'), + EventPattern('dbus-signal', signal='ServiceRemoved')]) + + call_async(q, conn.Future, 'EnsureSidecar', ycs.STATUS_IFACE) + + conn.Connect() + + # Now we're connected, the call we made earlier should return. + e = q.expect('dbus-return', method='EnsureSidecar') + path, props = e.value + assertEquals({}, props) + + status = ProxyWrapper(bus.get_object(conn.bus_name, path), + ycs.STATUS_IFACE, {}) + + # bad capability argument + call_async(q, status, 'AdvertiseStatus', '', 'service.name', '') + q.expect('dbus-error', method='AdvertiseStatus') + + # bad service name + call_async(q, status, 'AdvertiseStatus', CAP_NAME, '', '') + q.expect('dbus-error', method='AdvertiseStatus') + + # we can't test that the message type="headline" stanza is + # actually received because it's thrown into the loopback stream + # immediately. + + # announce a contact with the right caps + bare_jid = "test-status@example.com" + full_jid = bare_jid + "/BIGGESTRESOURCEEVAAAAHHH" + + contact_handle = conn.RequestHandles(cs.HT_CONTACT, [bare_jid])[0] + + presence_and_disco(q, conn, stream, full_jid, True, client, caps, + features, identity, {}, True, None) + + # this will be fired as text channel caps will be fired + q.expect('dbus-signal', signal='ContactCapabilitiesChanged', + predicate=lambda e: contact_handle in e.args[0]) + + # okay now we know about the contact's caps, we can go ahead + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredStatuses', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({}, discovered) + + el = Element(('urn:ytstenut:status', 'status')) + el['activity'] = 'messing-with-your-stuff' + desc = el.addElement('ytstenut:description', content='Yeah sorry about that') + desc['xml:lang'] = 'en-GB' + + call_async(q, status, 'AdvertiseStatus', CAP_NAME, + 'ants.in.their.pants', el.toXml()) + + e, _ = q.expect_many(EventPattern('stream-iq'), + EventPattern('dbus-return', method='AdvertiseStatus')) + + status_el, desc = check_pep_set(e.stanza) + assertEquals('messing-with-your-stuff', status_el['activity']) + assertEquals('ants.in.their.pants', status_el['from-service']) + assertEquals(CAP_NAME, status_el['capability']) + assertEquals('Yeah sorry about that', desc.children[0]) + + acknowledge_iq(stream, e.stanza) + send_back_pep_event(stream, status_el) + + sig = q.expect('dbus-signal', signal='StatusChanged', + interface=ycs.STATUS_IFACE) + + # check signal + contact_id, capability, service_name, status_str = sig.args + assertEquals(CAP_NAME, capability) + assertEquals('ants.in.their.pants', service_name) + assertNotEquals('', status_str) + + # check property + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredStatuses', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({'test@localhost': {CAP_NAME: {'ants.in.their.pants': status_str}}}, + discovered) + + # set another + el = Element(('urn:ytstenut:status', 'status')) + el['activity'] = 'rofling' + desc = el.addElement('ytstenut:description', content='U MAD?') + desc['xml:lang'] = 'en-GB' + + call_async(q, status, 'AdvertiseStatus', CAP_NAME, + 'bananaman.on.holiday', el.toXml()) + + e, _ = q.expect_many(EventPattern('stream-iq'), + EventPattern('dbus-return', method='AdvertiseStatus')) + + status_el, desc = check_pep_set(e.stanza) + assertEquals('rofling', status_el['activity']) + assertEquals('bananaman.on.holiday', status_el['from-service']) + assertEquals(CAP_NAME, status_el['capability']) + assertEquals('U MAD?', desc.children[0]) + + acknowledge_iq(stream, e.stanza) + send_back_pep_event(stream, status_el) + + sig = q.expect('dbus-signal', signal='StatusChanged', + interface=ycs.STATUS_IFACE) + + # check signal + contact_id, capability, service_name, bananaman_status_str = sig.args + assertEquals(CAP_NAME, capability) + assertEquals('bananaman.on.holiday', service_name) + assertNotEquals('', bananaman_status_str) + + # check property + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredStatuses', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({'test@localhost': {CAP_NAME: { + 'ants.in.their.pants': status_str, + 'bananaman.on.holiday': bananaman_status_str}}}, + discovered) + + # unset the status from one service + call_async(q, status, 'AdvertiseStatus', CAP_NAME, + 'ants.in.their.pants', '') + + e, _, = q.expect_many(EventPattern('stream-iq'), + EventPattern('dbus-return', method='AdvertiseStatus')) + + status_el, desc = check_pep_set(e.stanza) + assertEquals('ants.in.their.pants', status_el['from-service']) + assertEquals(CAP_NAME, status_el['capability']) + assert 'activity' not in status_el.attributes + assertEquals([], status_el.children) + + acknowledge_iq(stream, e.stanza) + send_back_pep_event(stream, status_el) + + sig = q.expect('dbus-signal', signal='StatusChanged', + interface=ycs.STATUS_IFACE) + + # check signal + contact_id, capability, service_name, status_str = sig.args + assertEquals(CAP_NAME, capability) + assertEquals('ants.in.their.pants', service_name) + assertEquals('', status_str) + + # check property + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredStatuses', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({'test@localhost': {CAP_NAME: { + 'bananaman.on.holiday': bananaman_status_str}}}, + discovered) + + # unset the status from the other service + call_async(q, status, 'AdvertiseStatus', CAP_NAME, + 'bananaman.on.holiday', '') + + e, _ = q.expect_many(EventPattern('stream-iq'), + EventPattern('dbus-return', method='AdvertiseStatus')) + + # check message + status_el, desc = check_pep_set(e.stanza) + assertEquals('bananaman.on.holiday', status_el['from-service']) + assertEquals(CAP_NAME, status_el['capability']) + assert 'activity' not in status_el.attributes + assertEquals([], status_el.children) + + acknowledge_iq(stream, e.stanza) + send_back_pep_event(stream, status_el) + + sig = q.expect('dbus-signal', signal='StatusChanged', + interface=ycs.STATUS_IFACE) + + # check signal + contact_id, capability, service_name, status_str = sig.args + assertEquals(CAP_NAME, capability) + assertEquals('bananaman.on.holiday', service_name) + assertEquals('', status_str) + + # check property + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredStatuses', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({}, discovered) + +if __name__ == '__main__': + exec_test(test, do_connect=False) diff --git a/tests/twisted/gabblecaps_helper.py b/tests/twisted/gabblecaps_helper.py new file mode 100644 index 0000000..6f80ba1 --- /dev/null +++ b/tests/twisted/gabblecaps_helper.py @@ -0,0 +1,356 @@ +# vim: set fileencoding=utf-8 : +import hashlib +import base64 +import dbus + +from twisted.words.xish import domish, xpath +from gabbletest import make_result_iq, make_presence, elem_iq, elem +from gabbleservicetest import ( + EventPattern, + assertEquals, assertContains, assertDoesNotContain, assertLength, + ) + +import config +import ns +import gabbleconstants 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): + """ + 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): + 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): + components.append(feature) + + for form_type in sorted(dataforms.keys()): + components.append(form_type) + + 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() + S = u'<'.join(components) + m.update(S.encode('utf-8')) + return base64.b64encode(m.digest()) + +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 + el['xml:lang'] = lang + + 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 + +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 + + return disco_caps(q, stream, presence) + (signaled_caps,) + +def extract_disco_parts(stanza): + identity_nodes = xpath.queryForNodes('/iq/query/identity', stanza) + assertLength(1, identity_nodes) + identity_node = identity_nodes[0] + + assertEquals('client', identity_node['category']) + assertDoesNotContain('xml:lang', identity_node.attributes) + + identity = 'client/%s//%s' % (identity_node['type'], identity_node['name']) + + features = [] + for feature in xpath.queryForNodes('/iq/query/feature', stanza): + features.append(feature['var']) + + # a quick and ugly data form extractor + x_nodes = xpath.queryForNodes('/iq/query/x', stanza) or [] + dataforms = {} + for form in x_nodes: + name = None + fields = {} + for field in xpath.queryForNodes('/x/field', form): + if field['var'] == 'FORM_TYPE': + name = str(field.firstChildElement()) + else: + values = [str(x) for x in xpath.queryForNodes('/field/value', field)] + + fields[field['var']] = values + + if name is not None: + dataforms[name] = fields + + return ([identity], features, dataforms) + +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. + (identities, features, dataforms) = extract_disco_parts(event.stanza) + + # Check if the hash matches the announced capabilities + assertEquals(compute_caps_hash(identities, features, dataforms), ver) + + return (event, features, dataforms) + +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 + 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/gabbleconstants.py b/tests/twisted/gabbleconstants.py new file mode 100644 index 0000000..aff01d1 --- /dev/null +++ b/tests/twisted/gabbleconstants.py @@ -0,0 +1,458 @@ +""" +Some handy constants for other tests to share and enjoy. +""" + +from dbus import PROPERTIES_IFACE + +CM = "org.freedesktop.Telepathy.ConnectionManager" + +HT_NONE = 0 +HT_CONTACT = 1 +HT_ROOM = 2 +HT_LIST = 3 +HT_GROUP = 4 + +CHANNEL = "org.freedesktop.Telepathy.Channel" + +CHANNEL_IFACE_CALL_STATE = CHANNEL + ".Interface.CallState" +CHANNEL_IFACE_CHAT_STATE = CHANNEL + '.Interface.ChatState' +CHANNEL_IFACE_DESTROYABLE = CHANNEL + ".Interface.Destroyable" +CHANNEL_IFACE_DTMF = CHANNEL + ".Interface.DTMF" +CHANNEL_IFACE_GROUP = CHANNEL + ".Interface.Group" +CHANNEL_IFACE_HOLD = CHANNEL + ".Interface.Hold" +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" +CHANNEL_IFACE_CONFERENCE = CHANNEL + '.Interface.Conference' +CHANNEL_IFACE_ROOM = CHANNEL + '.Interface.Room.DRAFT' + +CHANNEL_TYPE_CALL = CHANNEL + ".Type.Call.DRAFT" +CHANNEL_TYPE_CONTACT_LIST = CHANNEL + ".Type.ContactList" +CHANNEL_TYPE_CONTACT_SEARCH = CHANNEL + ".Type.ContactSearch" +CHANNEL_TYPE_TEXT = CHANNEL + ".Type.Text" +CHANNEL_TYPE_TUBES = CHANNEL + ".Type.Tubes" +CHANNEL_TYPE_STREAM_TUBE = CHANNEL + ".Type.StreamTube" +CHANNEL_TYPE_DBUS_TUBE = CHANNEL + ".Type.DBusTube" +CHANNEL_TYPE_STREAMED_MEDIA = CHANNEL + ".Type.StreamedMedia" +CHANNEL_TYPE_TEXT = CHANNEL + ".Type.Text" +CHANNEL_TYPE_FILE_TRANSFER = CHANNEL + ".Type.FileTransfer" +CHANNEL_TYPE_SERVER_AUTHENTICATION = \ + CHANNEL + ".Type.ServerAuthentication" +CHANNEL_TYPE_SERVER_TLS_CONNECTION = \ + CHANNEL + ".Type.ServerTLSConnection" + +TP_AWKWARD_PROPERTIES = "org.freedesktop.Telepathy.Properties" +PROPERTY_FLAG_READ = 1 +PROPERTY_FLAG_WRITE = 2 +PROPERTY_FLAGS_RW = PROPERTY_FLAG_READ | PROPERTY_FLAG_WRITE + +CHANNEL_TYPE = CHANNEL + '.ChannelType' +TARGET_HANDLE_TYPE = CHANNEL + '.TargetHandleType' +TARGET_HANDLE = CHANNEL + '.TargetHandle' +TARGET_ID = CHANNEL + '.TargetID' +REQUESTED = CHANNEL + '.Requested' +INITIATOR_HANDLE = CHANNEL + '.InitiatorHandle' +INITIATOR_ID = CHANNEL + '.InitiatorID' +INTERFACES = CHANNEL + '.Interfaces' + +INITIAL_AUDIO = CHANNEL_TYPE_STREAMED_MEDIA + '.InitialAudio' +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_IFACE_MEDIA = \ + 'org.freedesktop.Telepathy.Call.Content.Interface.Media.DRAFT' + +CALL_CONTENT_CODECOFFER = \ + 'org.freedesktop.Telepathy.Call.Content.CodecOffer.DRAFT' + +CALL_STREAM = 'org.freedesktop.Telepathy.Call.Stream.DRAFT' +CALL_STREAM_IFACE_MEDIA = \ + 'org.freedesktop.Telepathy.Call.Stream.Interface.Media.DRAFT' + +CALL_STREAM_ENDPOINT = 'org.freedesktop.Telepathy.Call.Stream.Endpoint.DRAFT' + +CALL_MEDIA_TYPE_AUDIO = 0 +CALL_MEDIA_TYPE_VIDEO = 1 + +CALL_CONTENT_PACKETIZATION_RTP = 0 +CALL_CONTENT_PACKETIZATION_RAW = 1 +CALL_CONTENT_PACKETIZATION_MSN_WEBCAM = 2 + +CALL_STREAM_TRANSPORT_RAW_UDP = 1 +CALL_STREAM_TRANSPORT_ICE = 2 +CALL_STREAM_TRANSPORT_GOOGLE = 3 + +CALL_STATE_UNKNOWN = 0 +CALL_STATE_PENDING_INITIATOR = 1 +CALL_STATE_PENDING_RECEIVER = 2 +CALL_STATE_ACCEPTED = 3 +CALL_STATE_ENDED = 4 + +CALL_MEMBER_FLAG_RINGING = 1 +CALL_MEMBER_FLAG_HELD = 2 + +CALL_DISPOSITION_NONE = 0 +CALL_DISPOSITION_INITIAL = 1 + +CALL_SENDING_STATE_NONE = 0 +CALL_SENDING_STATE_PENDING_SEND = 1 +CALL_SENDING_STATE_SENDING = 2 + +SUBSCRIPTION_STATE_UNKNOWN = 0 +SUBSCRIPTION_STATE_NO = 1 +SUBSCRIPTION_STATE_REMOVED_REMOTELY = 2 +SUBSCRIPTION_STATE_ASK = 3 +SUBSCRIPTION_STATE_YES = 4 + +CONTACT_LIST_STATE_NONE = 0 +CONTACT_LIST_STATE_WAITING = 1 +CONTACT_LIST_STATE_FAILURE = 2 +CONTACT_LIST_STATE_SUCCESS = 3 + +CONN = "org.freedesktop.Telepathy.Connection" +CONN_IFACE_AVATARS = CONN + '.Interface.Avatars' +CONN_IFACE_ALIASING = CONN + '.Interface.Aliasing' +CONN_IFACE_CAPS = CONN + '.Interface.Capabilities' +CONN_IFACE_CONTACTS = CONN + '.Interface.Contacts' +CONN_IFACE_CONTACT_CAPS = CONN + '.Interface.ContactCapabilities' +CONN_IFACE_CONTACT_INFO = CONN + ".Interface.ContactInfo" +CONN_IFACE_PRESENCE = CONN + '.Interface.Presence' +CONN_IFACE_SIMPLE_PRESENCE = CONN + '.Interface.SimplePresence' +CONN_IFACE_REQUESTS = CONN + '.Interface.Requests' +CONN_IFACE_LOCATION = CONN + '.Interface.Location' +CONN_IFACE_GABBLE_DECLOAK = CONN + '.Interface.Gabble.Decloak' +CONN_IFACE_MAIL_NOTIFICATION = CONN + '.Interface.MailNotification' +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' + +ATTR_CONTACT_CAPABILITIES = CONN_IFACE_CONTACT_CAPS + '/capabilities' + +STREAM_HANDLER = 'org.freedesktop.Telepathy.Media.StreamHandler' + +ERROR = 'org.freedesktop.Telepathy.Error' +INVALID_ARGUMENT = ERROR + '.InvalidArgument' +NOT_IMPLEMENTED = ERROR + '.NotImplemented' +NOT_AVAILABLE = ERROR + '.NotAvailable' +PERMISSION_DENIED = ERROR + '.PermissionDenied' +OFFLINE = ERROR + '.Offline' +NOT_CAPABLE = ERROR + '.NotCapable' +CONNECTION_REFUSED = ERROR + '.ConnectionRefused' +CONNECTION_FAILED = ERROR + '.ConnectionFailed' +CONNECTION_LOST = ERROR + '.ConnectionLost' +CANCELLED = ERROR + '.Cancelled' +DISCONNECTED = ERROR + '.Disconnected' +REGISTRATION_EXISTS = ERROR + '.RegistrationExists' +AUTHENTICATION_FAILED = ERROR + '.AuthenticationFailed' +CONNECTION_REPLACED = ERROR + '.ConnectionReplaced' +ALREADY_CONNECTED = ERROR + '.AlreadyConnected' +NETWORK_ERROR = ERROR + '.NetworkError' +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' + +TUBE_PARAMETERS = CHANNEL_IFACE_TUBE + '.Parameters' +TUBE_STATE = CHANNEL_IFACE_TUBE + '.State' +STREAM_TUBE_SERVICE = CHANNEL_TYPE_STREAM_TUBE + '.Service' +DBUS_TUBE_SERVICE_NAME = CHANNEL_TYPE_DBUS_TUBE + '.ServiceName' +DBUS_TUBE_DBUS_NAMES = CHANNEL_TYPE_DBUS_TUBE + '.DBusNames' +DBUS_TUBE_SUPPORTED_ACCESS_CONTROLS = CHANNEL_TYPE_DBUS_TUBE + '.SupportedAccessControls' +STREAM_TUBE_SUPPORTED_SOCKET_TYPES = CHANNEL_TYPE_STREAM_TUBE + '.SupportedSocketTypes' + +CONFERENCE_INITIAL_CHANNELS = CHANNEL_IFACE_CONFERENCE + '.InitialChannels' +CONFERENCE_INITIAL_INVITEE_HANDLES = CHANNEL_IFACE_CONFERENCE + '.InitialInviteeHandles' +CONFERENCE_INITIAL_INVITEE_IDS = CHANNEL_IFACE_CONFERENCE + '.InitialInviteeIDs' + +CONTACT_SEARCH_ASK = CHANNEL_TYPE_CONTACT_SEARCH + '.AvailableSearchKeys' +CONTACT_SEARCH_SERVER = CHANNEL_TYPE_CONTACT_SEARCH + '.Server' +CONTACT_SEARCH_STATE = CHANNEL_TYPE_CONTACT_SEARCH + '.SearchState' + +SEARCH_NOT_STARTED = 0 +SEARCH_IN_PROGRESS = 1 +SEARCH_MORE_AVAILABLE = 2 +SEARCH_COMPLETED = 3 +SEARCH_FAILED = 4 + +TUBE_CHANNEL_STATE_LOCAL_PENDING = 0 +TUBE_CHANNEL_STATE_REMOTE_PENDING = 1 +TUBE_CHANNEL_STATE_OPEN = 2 +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 +SOCKET_ADDRESS_TYPE_IPV6 = 3 + +SOCKET_ACCESS_CONTROL_LOCALHOST = 0 +SOCKET_ACCESS_CONTROL_PORT = 1 +SOCKET_ACCESS_CONTROL_NETMASK = 2 +SOCKET_ACCESS_CONTROL_CREDENTIALS = 3 + +TUBE_STATE_LOCAL_PENDING = 0 +TUBE_STATE_REMOTE_PENDING = 1 +TUBE_STATE_OPEN = 2 +TUBE_STATE_NOT_OFFERED = 3 + +TUBE_TYPE_DBUS = 0 +TUBE_TYPE_STREAM = 1 + +MEDIA_STREAM_DIRECTION_NONE = 0 +MEDIA_STREAM_DIRECTION_SEND = 1 +MEDIA_STREAM_DIRECTION_RECEIVE = 2 +MEDIA_STREAM_DIRECTION_BIDIRECTIONAL = 3 + +MEDIA_STREAM_PENDING_LOCAL_SEND = 1 +MEDIA_STREAM_PENDING_REMOTE_SEND = 2 + +MEDIA_STREAM_TYPE_AUDIO = 0 +MEDIA_STREAM_TYPE_VIDEO = 1 + +MEDIA_STREAM_STATE_DISCONNECTED = 0 +MEDIA_STREAM_STATE_CONNECTING = 1 +MEDIA_STREAM_STATE_CONNECTED = 2 + +MEDIA_STREAM_DIRECTION_NONE = 0 +MEDIA_STREAM_DIRECTION_SEND = 1 +MEDIA_STREAM_DIRECTION_RECEIVE = 2 +MEDIA_STREAM_DIRECTION_BIDIRECTIONAL = 3 + +FT_STATE_NONE = 0 +FT_STATE_PENDING = 1 +FT_STATE_ACCEPTED = 2 +FT_STATE_OPEN = 3 +FT_STATE_COMPLETED = 4 +FT_STATE_CANCELLED = 5 + +FT_STATE_CHANGE_REASON_NONE = 0 +FT_STATE_CHANGE_REASON_REQUESTED = 1 +FT_STATE_CHANGE_REASON_LOCAL_STOPPED = 2 +FT_STATE_CHANGE_REASON_REMOTE_STOPPED = 3 +FT_STATE_CHANGE_REASON_LOCAL_ERROR = 4 +FT_STATE_CHANGE_REASON_REMOTE_ERROR = 5 + +FILE_HASH_TYPE_NONE = 0 +FILE_HASH_TYPE_MD5 = 1 +FILE_HASH_TYPE_SHA1 = 2 +FILE_HASH_TYPE_SHA256 = 3 + +FT_STATE = CHANNEL_TYPE_FILE_TRANSFER + '.State' +FT_CONTENT_TYPE = CHANNEL_TYPE_FILE_TRANSFER + '.ContentType' +FT_FILENAME = CHANNEL_TYPE_FILE_TRANSFER + '.Filename' +FT_SIZE = CHANNEL_TYPE_FILE_TRANSFER + '.Size' +FT_CONTENT_HASH_TYPE = CHANNEL_TYPE_FILE_TRANSFER + '.ContentHashType' +FT_CONTENT_HASH = CHANNEL_TYPE_FILE_TRANSFER + '.ContentHash' +FT_DESCRIPTION = CHANNEL_TYPE_FILE_TRANSFER + '.Description' +FT_DATE = CHANNEL_TYPE_FILE_TRANSFER + '.Date' +FT_AVAILABLE_SOCKET_TYPES = CHANNEL_TYPE_FILE_TRANSFER + '.AvailableSocketTypes' +FT_TRANSFERRED_BYTES = CHANNEL_TYPE_FILE_TRANSFER + '.TransferredBytes' +FT_INITIAL_OFFSET = CHANNEL_TYPE_FILE_TRANSFER + '.InitialOffset' +FT_FILE_COLLECTION = CHANNEL_TYPE_FILE_TRANSFER + '.FUTURE.FileCollection' +FT_URI = CHANNEL_TYPE_FILE_TRANSFER + '.URI' + +GF_CAN_ADD = 1 +GF_CAN_REMOVE = 2 +GF_CAN_RESCIND = 4 +GF_MESSAGE_ADD = 8 +GF_MESSAGE_REMOVE = 16 +GF_MESSAGE_ACCEPT = 32 +GF_MESSAGE_REJECT = 64 +GF_MESSAGE_RESCIND = 128 +GF_CHANNEL_SPECIFIC_HANDLES = 256 +GF_ONLY_ONE_GROUP = 512 +GF_HANDLE_OWNERS_NOT_AVAILABLE = 1024 +GF_PROPERTIES = 2048 +GF_MEMBERS_CHANGED_DETAILED = 4096 + +GC_REASON_NONE = 0 +GC_REASON_OFFLINE = 1 +GC_REASON_KICKED = 2 +GC_REASON_BUSY = 3 +GC_REASON_INVITED = 4 +GC_REASON_BANNED = 5 +GC_REASON_ERROR = 6 +GC_REASON_INVALID_CONTACT = 7 +GC_REASON_NO_ANSWER = 8 +GC_REASON_RENAMED = 9 +GC_REASON_PERMISSION_DENIED = 10 +GC_REASON_SEPARATED = 11 + +HS_UNHELD = 0 +HS_HELD = 1 +HS_PENDING_HOLD = 2 +HS_PENDING_UNHOLD = 3 + +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 + +CSR_NONE_SPECIFIED = 0 +CSR_REQUESTED = 1 +CSR_NETWORK_ERROR = 2 +CSR_AUTHENTICATION_FAILED = 3 +CSR_ENCRYPTION_ERROR = 4 +CSR_NAME_IN_USE = 5 +CSR_CERT_NOT_PROVIDED = 6 +CSR_CERT_UNTRUSTED = 7 +CSR_CERT_EXPIRED = 8 +CSR_CERT_NOT_ACTIVATED = 9 +CSR_CERT_HOSTNAME_MISMATCH = 10 +CSR_CERT_FINGERPRINT_MISMATCH = 11 +CSR_CERT_SELF_SIGNED = 12 +CSR_CERT_OTHER_ERROR = 13 + +BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' +ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties' + +CHAT_STATE_GONE = 0 +CHAT_STATE_INACTIVE = 1 +CHAT_STATE_ACTIVE = 2 +CHAT_STATE_PAUSED = 3 +CHAT_STATE_COMPOSING = 4 + +# Channel_Media_Capabilities +MEDIA_CAP_AUDIO = 1 +MEDIA_CAP_VIDEO = 2 +MEDIA_CAP_STUN = 4 +MEDIA_CAP_GTALKP2P = 8 +MEDIA_CAP_ICEUDP = 16 +MEDIA_CAP_IMMUTABLE_STREAMS = 32 + +CLIENT = 'org.freedesktop.Telepathy.Client' + +PRESENCE_OFFLINE = 1 +PRESENCE_AVAILABLE = 2 +PRESENCE_AWAY = 3 +PRESENCE_EXTENDED_AWAY = 4 +PRESENCE_HIDDEN = 5 +PRESENCE_BUSY = 6 +PRESENCE_UNKNOWN = 7 +PRESENCE_ERROR = 8 + +CONTACT_INFO_FLAG_CAN_SET = 1 +CONTACT_INFO_FLAG_PUSH = 2 +CONTACT_INFO_FIELD_FLAG_PARAMETERS_EXACT = 1 +CONTACT_INFO_FIELD_FLAG_OVERWRITTEN_BY_NICKNAME = 2 + +# Channel_Interface_SaslAuthentication +SASL_STATUS_NOT_STARTED = 0 +SASL_STATUS_IN_PROGRESS = 1 +SASL_STATUS_SERVER_SUCCEEDED = 2 +SASL_STATUS_CLIENT_ACCEPTED = 3 +SASL_STATUS_SUCCEEDED = 4 +SASL_STATUS_SERVER_FAILED = 5 +SASL_STATUS_CLIENT_FAILED = 6 + +SASL_ABORT_REASON_INVALID_CHALLENGE = 0 +SASL_ABORT_REASON_USER_ABORT = 1 + +AUTH_METHOD = CHANNEL_TYPE_SERVER_AUTHENTICATION + ".AuthenticationMethod" +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 + +LOCATION_FEATURE_CAN_SET = 1 + +# Channel.Type.Text + +MT_NORMAL = 0 +MT_ACTION = 1 +MT_NOTICE = 2 +MT_AUTO_REPLY = 3 +MT_DELIVERY_REPORT = 4 + +PROTOCOL = 'org.freedesktop.Telepathy.Protocol' +PROTOCOL_IFACE_PRESENCES = PROTOCOL + '.Interface.Presence' +PARAM_REQUIRED = 1 +PARAM_REGISTER = 2 +PARAM_HAS_DEFAULT = 4 +PARAM_SECRET = 8 +PARAM_DBUS_PROPERTY = 16 + +AUTHENTICATION = 'org.freedesktop.Telepathy.Authentication' +AUTH_TLS_CERT = AUTHENTICATION + ".TLSCertificate" + +TLS_CERT_STATE_PENDING = 0 +TLS_CERT_STATE_ACCEPTED = 1 +TLS_CERT_STATE_REJECTED = 2 + +TLS_REJECT_REASON_UNKNOWN = 0 +TLS_REJECT_REASON_UNTRUSTED = 1 + +# Channel.Interface.Messages + +MESSAGE_PART_SUPPORT_FLAGS = CHANNEL_IFACE_MESSAGES + '.MessagePartSupportFlags' +DELIVERY_REPORTING_SUPPORT = CHANNEL_IFACE_MESSAGES + '.DeliveryReportingSupport' +SUPPORTED_CONTENT_TYPES = CHANNEL_IFACE_MESSAGES + '.SupportedContentTypes' + +MSG_SENDING_FLAGS_REPORT_DELIVERY = 1 +MSG_SENDING_FLAGS_REPORT_READ = 2 +MSG_SENDING_FLAGS_REPORT_DELETED = 4 + +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 + +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_ROOM_ID = CHANNEL_IFACE_ROOM + '.RoomID' +ROOM_SERVER = CHANNEL_IFACE_ROOM + '.Server' +ROOM_SUBJECT = CHANNEL_IFACE_ROOM + '.Subject' diff --git a/tests/twisted/gabbleservicetest.py b/tests/twisted/gabbleservicetest.py new file mode 100644 index 0000000..8d217ca --- /dev/null +++ b/tests/twisted/gabbleservicetest.py @@ -0,0 +1,640 @@ + +""" +Infrastructure code for testing connection managers. +""" + +from twisted.internet import glib2reactor +from twisted.internet.protocol import Protocol, Factory, ClientFactory +glib2reactor.install() +import sys +import time + +import pprint +import unittest + +import dbus.glib + +from twisted.internet import reactor + +import gabbleconstants as cs + +tp_name_prefix = 'org.freedesktop.Telepathy' +tp_path_prefix = '/org/freedesktop/Telepathy' + +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 sorted(dir(event)): + if key != 'type' and not key.startswith('_'): + ret.append('- %s: %s' % ( + key, pprint.pformat(getattr(event, key)))) + + if key == 'error': + ret.append('%s' % getattr(event, key)) + + return ret + +class EventPattern: + def __init__(self, type, **properties): + self.type = type + self.predicate = None + if 'predicate' in properties: + self.predicate = properties['predicate'] + del properties['predicate'] + self.properties = properties + (self.subqueue, self.subtype) = type.split ("-", 1) + + def __repr__(self): + properties = dict(self.properties) + + if self.predicate is not None: + properties['predicate'] = self.predicate + + return '%s(%r, **%r)' % ( + self.__class__.__name__, self.type, properties) + + def match(self, event): + if event.type != self.type: + return False + + for key, value in self.properties.iteritems(): + try: + if getattr(event, key) != value: + return False + except AttributeError: + return False + + if self.predicate is None or self.predicate(event): + return True + + return False + + +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. + + Implement the wait() method to have something that works. + """ + + def __init__(self, timeout=None): + self.verbose = False + self.forbidden_events = set() + self.event_queues = {} + + if timeout is None: + self.timeout = 5 + else: + self.timeout = timeout + + def log(self, s): + if self.verbose: + print s + + def log_queues(self, queues): + self.log ("Waiting for event on: %s" % ", ".join(queues)) + + def log_event(self, event): + self.log('got event:') + + if self.verbose: + map(self.log, format_event(event)) + + def forbid_events(self, patterns): + """ + Add patterns (an iterable of EventPattern) to the set of forbidden + events. If a forbidden event occurs during an expect or expect_many, + the test will fail. + """ + self.forbidden_events.update(set(patterns)) + + def unforbid_events(self, patterns): + """ + Remove 'patterns' (an iterable of EventPattern) from the set of + forbidden events. These must be the same EventPattern pointers that + were passed to forbid_events. + """ + self.forbidden_events.difference_update(set(patterns)) + + def _check_forbidden(self, event): + for e in self.forbidden_events: + if e.match(event): + raise ForbiddenEventOccurred(event) + + def expect(self, type, **kw): + """ + Waits for an event matching the supplied pattern to occur, and returns + it. For example, to await a D-Bus signal with particular arguments: + + e = q.expect('dbus-signal', signal='Badgers', args=["foo", 42]) + """ + pattern = EventPattern(type, **kw) + t = time.time() + + while True: + event = self.wait([pattern.subqueue]) + self._check_forbidden(event) + + if pattern.match(event): + self.log('handled, took %0.3f ms' + % ((time.time() - t) * 1000.0) ) + self.log('') + return event + + self.log('not handled') + self.log('') + + def expect_many(self, *patterns): + """ + Waits for events matching all of the supplied EventPattern instances to + return, and returns a list of events in the same order as the patterns + they matched. After a pattern is successfully matched, it is not + considered for future events; if more than one unsatisfied pattern + matches an event, the first "wins". + + Note that the expected events may occur in any order. If you're + expecting a series of events in a particular order, use repeated calls + to expect() instead. + + This method is useful when you're awaiting a number of events which may + happen in any order. For instance, in telepathy-gabble, calling a D-Bus + method often causes a value to be returned immediately, as well as a + query to be sent to the server. Since these events may reach the test + in either order, the following is incorrect and will fail if the IQ + happens to reach the test first: + + ret = q.expect('dbus-return', method='Foo') + query = q.expect('stream-iq', query_ns=ns.FOO) + + The following would be correct: + + ret, query = q.expect_many( + EventPattern('dbus-return', method='Foo'), + EventPattern('stream-iq', query_ns=ns.FOO), + ) + """ + ret = [None] * len(patterns) + t = time.time() + + while None in ret: + try: + queues = set() + for i, pattern in enumerate(patterns): + if ret[i] is None: + queues.add(pattern.subqueue) + event = self.wait(queues) + except TimeoutError: + self.log('timeout') + self.log('still expecting:') + for i, pattern in enumerate(patterns): + if ret[i] is None: + self.log(' - %r' % pattern) + raise + self._check_forbidden(event) + + for i, pattern in enumerate(patterns): + if ret[i] is None and pattern.match(event): + self.log('handled, took %0.3f ms' + % ((time.time() - t) * 1000.0) ) + self.log('') + ret[i] = event + break + else: + self.log('not handled') + self.log('') + + return ret + + def demand(self, type, **kw): + pattern = EventPattern(type, **kw) + + event = self.wait([pattern.subqueue]) + + if pattern.match(event): + self.log('handled') + self.log('') + return event + + self.log('not handled') + raise RuntimeError('expected %r, got %r' % (pattern, event)) + + def queues_available(self, queues): + if queues == None: + return self.event_queues.keys() + else: + available = self.event_queues.keys() + return filter(lambda x: x in available, queues) + + + def pop_next(self, queue): + events = self.event_queues[queue] + e = events.pop(0) + if not events: + self.event_queues.pop (queue) + return e + + def append(self, event): + self.log ("Adding to queue") + self.log_event (event) + self.event_queues[event.subqueue] = \ + self.event_queues.get(event.subqueue, []) + [event] + +class IteratingEventQueue(BaseEventQueue): + """Event queue that works by iterating the Twisted reactor.""" + + def __init__(self, timeout=None): + BaseEventQueue.__init__(self, timeout) + + def wait(self, queues=None): + stop = [False] + + def later(): + stop[0] = True + + delayed_call = reactor.callLater(self.timeout, later) + + self.log_queues(queues) + + qa = self.queues_available(queues) + while not qa and (not stop[0]): + reactor.iterate(0.01) + qa = self.queues_available(queues) + + if qa: + delayed_call.cancel() + e = self.pop_next (qa[0]) + self.log_event (e) + return e + else: + raise TimeoutError + +class TestEventQueue(BaseEventQueue): + def __init__(self, events): + BaseEventQueue.__init__(self) + for e in events: + self.append (e) + + def wait(self, queues = None): + qa = self.queues_available(queues) + + if qa: + return self.pop_next (qa[0]) + else: + raise TimeoutError + +class EventQueueTest(unittest.TestCase): + def test_expect(self): + queue = TestEventQueue([Event('test-foo'), Event('test-bar')]) + assert queue.expect('test-foo').type == 'test-foo' + assert queue.expect('test-bar').type == 'test-bar' + + def test_expect_many(self): + queue = TestEventQueue([Event('test-foo'), + Event('test-bar')]) + bar, foo = queue.expect_many( + EventPattern('test-bar'), + EventPattern('test-foo')) + assert bar.type == 'test-bar' + assert foo.type == 'test-foo' + + def test_expect_many2(self): + # Test that events are only matched against patterns that haven't yet + # been matched. This tests a regression. + queue = TestEventQueue([Event('test-foo', x=1), Event('test-foo', x=2)]) + foo1, foo2 = queue.expect_many( + EventPattern('test-foo'), + EventPattern('test-foo')) + assert foo1.type == 'test-foo' and foo1.x == 1 + assert foo2.type == 'test-foo' and foo2.x == 2 + + def test_expect_queueing(self): + queue = TestEventQueue([Event('foo-test', x=1), + Event('foo-test', x=2)]) + + queue.append(Event('bar-test', x=1)) + queue.append(Event('bar-test', x=2)) + + queue.append(Event('baz-test', x=1)) + queue.append(Event('baz-test', x=2)) + + for x in xrange(1,2): + e = queue.expect ('baz-test') + assertEquals (x, e.x) + + e = queue.expect ('bar-test') + assertEquals (x, e.x) + + e = queue.expect ('foo-test') + assertEquals (x, e.x) + + def test_timeout(self): + queue = TestEventQueue([]) + self.assertRaises(TimeoutError, queue.expect, 'test-foo') + + def test_demand(self): + queue = TestEventQueue([Event('test-foo'), Event('test-bar')]) + foo = queue.demand('test-foo') + assert foo.type == 'test-foo' + + def test_demand_fail(self): + queue = TestEventQueue([Event('test-foo'), Event('test-bar')]) + self.assertRaises(RuntimeError, queue.demand, 'test-bar') + +def unwrap(x): + """Hack to unwrap D-Bus values, so that they're easier to read when + printed.""" + + if isinstance(x, list): + return map(unwrap, x) + + if isinstance(x, tuple): + return tuple(map(unwrap, x)) + + if isinstance(x, dict): + return dict([(unwrap(k), unwrap(v)) for k, v in x.iteritems()]) + + if isinstance(x, dbus.Boolean): + return bool(x) + + for t in [unicode, str, long, int, float]: + if isinstance(x, t): + return t(x) + + return x + +def call_async(test, proxy, method, *args, **kw): + """Call a D-Bus method asynchronously and generate an event for the + resulting method return/error.""" + + def reply_func(*ret): + test.append(Event('dbus-return', method=method, + value=unwrap(ret))) + + def error_func(err): + test.append(Event('dbus-error', method=method, error=err, + name=err.get_dbus_name(), message=str(err))) + + method_proxy = getattr(proxy, method) + kw.update({'reply_handler': reply_func, 'error_handler': error_func}) + method_proxy(*args, **kw) + +def sync_dbus(bus, q, conn): + # 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, '/', 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={}): + self.object = object + self.default_interface = dbus.Interface(object, default) + self.Properties = dbus.Interface(object, dbus.PROPERTIES_IFACE) + self.TpProperties = \ + dbus.Interface(object, tp_name_prefix + '.Properties') + self.interfaces = dict([ + (name, dbus.Interface(object, iface)) + for name, iface in others.iteritems()]) + + def __getattr__(self, name): + if name in self.interfaces: + return self.interfaces[name] + + if name in self.object.__dict__: + return getattr(self.object, name) + + return getattr(self.default_interface, name) + +def wrap_connection(conn): + return ProxyWrapper(conn, tp_name_prefix + '.Connection', + dict([ + (name, tp_name_prefix + '.Connection.Interface.' + name) + for name in ['Aliasing', 'Avatars', 'Capabilities', 'Contacts', + 'Presence', 'SimplePresence', 'Requests']] + + [('Peer', 'org.freedesktop.DBus.Peer'), + ('ContactCapabilities', cs.CONN_IFACE_CONTACT_CAPS), + ('ContactInfo', cs.CONN_IFACE_CONTACT_INFO), + ('Location', cs.CONN_IFACE_LOCATION), + ('Future', tp_name_prefix + '.Connection.FUTURE'), + ('MailNotification', cs.CONN_IFACE_MAIL_NOTIFICATION), + ('ContactList', cs.CONN_IFACE_CONTACT_LIST), + ('ContactGroups', cs.CONN_IFACE_CONTACT_GROUPS), + ('PowerSaving', cs.CONN_IFACE_POWER_SAVING), + ])) + +def wrap_channel(chan, type_, extra=None): + interfaces = { + type_: tp_name_prefix + '.Channel.Type.' + type_, + 'Group': tp_name_prefix + '.Channel.Interface.Group', + } + + if extra: + interfaces.update(dict([ + (name, tp_name_prefix + '.Channel.Interface.' + name) + for name in extra])) + + return ProxyWrapper(chan, tp_name_prefix + '.Channel', interfaces) + +def make_connection(bus, event_func, name, proto, params): + cm = bus.get_object( + tp_name_prefix + '.ConnectionManager.%s' % name, + 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, dbus.Dictionary(params, signature='sv')) + conn = wrap_connection(bus.get_object(connection_name, connection_path)) + + return conn + +def make_channel_proxy(conn, path, iface): + bus = dbus.SessionBus() + chan = bus.get_object(conn.object.bus_name, path) + chan = dbus.Interface(chan, tp_name_prefix + '.' + iface) + return chan + +# block_reading can be used if the test want to choose when we start to read +# data from the socket. +class EventProtocol(Protocol): + def __init__(self, queue=None, block_reading=False): + self.queue = queue + self.block_reading = block_reading + + def dataReceived(self, data): + if self.queue is not None: + self.queue.append(Event('socket-data', protocol=self, + data=data)) + + def sendData(self, data): + self.transport.write(data) + + def connectionMade(self): + if self.block_reading: + self.transport.stopReading() + + def connectionLost(self, reason=None): + if self.queue is not None: + self.queue.append(Event('socket-disconnected', protocol=self)) + +class EventProtocolFactory(Factory): + def __init__(self, queue, block_reading=False): + self.queue = queue + self.block_reading = block_reading + + def _create_protocol(self): + return EventProtocol(self.queue, self.block_reading) + + def buildProtocol(self, addr): + proto = self._create_protocol() + self.queue.append(Event('socket-connected', protocol=proto)) + return proto + +class EventProtocolClientFactory(EventProtocolFactory, ClientFactory): + pass + +def watch_tube_signals(q, tube): + def got_signal_cb(*args, **kwargs): + q.append(Event('tube-signal', + path=kwargs['path'], + signal=kwargs['member'], + args=map(unwrap, args), + tube=tube)) + + tube.add_signal_receiver(got_signal_cb, + path_keyword='path', member_keyword='member', + byte_arrays=True) + +def pretty(x): + return pprint.pformat(unwrap(x)) + +def assertEquals(expected, value): + if expected != value: + raise AssertionError( + "expected:\n%s\ngot:\n%s" % (pretty(expected), pretty(value))) + +def assertSameSets(expected, value): + exp_set = set(expected) + val_set = set(value) + + if exp_set != val_set: + raise AssertionError( + "expected contents:\n%s\ngot:\n%s" % ( + pretty(exp_set), pretty(val_set))) + +def assertNotEquals(expected, value): + if expected == value: + raise AssertionError( + "expected something other than:\n%s" % pretty(value)) + +def assertContains(element, value): + if element not in value: + raise AssertionError( + "expected:\n%s\nin:\n%s" % (pretty(element), pretty(value))) + +def assertDoesNotContain(element, value): + if element in value: + raise AssertionError( + "expected:\n%s\nnot in:\n%s" % (pretty(element), pretty(value))) + +def assertLength(length, value): + if len(value) != length: + raise AssertionError("expected: length %d, got length %d:\n%s" % ( + length, len(value), pretty(value))) + +def assertFlagsSet(flags, value): + masked = value & flags + if masked != flags: + raise AssertionError( + "expected flags %u, of which only %u are set in %u" % ( + flags, masked, value)) + +def assertFlagsUnset(flags, value): + masked = value & flags + if masked != 0: + raise AssertionError( + "expected none of flags %u, but %u are set in %u" % ( + flags, masked, value)) + +def 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 + + def green(s): + return '\x1b[32m%s\x1b[0m' % s + + patterns = { + 'handled': green, + 'not handled': red, + } + + class Colourer: + def __init__(self, fh, patterns): + self.fh = fh + self.patterns = patterns + + def write(self, s): + for p, f in self.patterns.items(): + if s.startswith(p): + self.fh.write(f(p) + s[len(p):]) + return + + self.fh.write(s) + + sys.stdout = Colourer(sys.stdout, patterns) + return sys.stdout + +if __name__ == '__main__': + unittest.main() + diff --git a/tests/twisted/gabbletest.py b/tests/twisted/gabbletest.py new file mode 100644 index 0000000..a635d19 --- /dev/null +++ b/tests/twisted/gabbletest.py @@ -0,0 +1,824 @@ + +""" +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 gabbleconstants as cs +import gabbleservicetest as servicetest +from gabbleservicetest 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.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@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)) + +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 _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) + 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) + ) + + 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 exec_test_deferred(fun, params, protocol=None, timeout=None, + authenticator=None, num_instances=1, + do_connect=True): + # 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, 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() + queue.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTING, cs.CSR_REQUESTED]) + queue.expect('stream-authenticated') + queue.expect('dbus-signal', signal='PresenceUpdate', + args=[{1L: (0L, {u'available': {}})}]) + queue.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + 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/salut/slow-service.py b/tests/twisted/salut/slow-service.py new file mode 100644 index 0000000..ac85424 --- /dev/null +++ b/tests/twisted/salut/slow-service.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +import dbus + +from salutservicetest import call_async, EventPattern, assertEquals, \ + ProxyWrapper, assertNotEquals, assertSameSets +from saluttest import exec_test, wait_for_contact_in_publish, make_result_iq, \ + sync_stream +import salutconstants as cs +import yconstants as ycs +from caps_helper import * + +from twisted.words.xish import xpath +from twisted.words.xish.domish import Element +from twisted.words.xish import domish + +from avahitest import AvahiAnnouncer, AvahiListener +from avahitest import get_host_name +from xmppstream import setup_stream_listener, connect_to_stream + +CLIENT_NAME = 'il-cliente-del-futuro' + +banshee = { + 'urn:ytstenut:capabilities#org.gnome.Banshee': + {'type': ['application'], + 'name': ['en_GB/Banshee Media Player', + 'fr/Banshee Lecteur de Musique'], + 'capabilities': ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'] + } +} + +evince = { + 'urn:ytstenut:capabilities#org.gnome.Evince': + {'type': ['application'], + 'name': ['en_GB/Evince Picture Viewer', + 'fr/Evince uh, ow do you say'], + 'capabilities': ['urn:ytstenut:capabilities:pics'], + } +} + +def test(q, bus, conn): + forbidden = [EventPattern('dbus-signal', signal='ServiceAdded'), + EventPattern('dbus-signal', signal='ServiceRemoved')] + q.forbid_events(forbidden) + + conn.Connect() + + q.expect('dbus-signal', signal='StatusChanged', args=[0, 0]) + + # announce a contact with the right caps + ver = compute_caps_hash([], [], banshee) + txt_record = { "txtvers": "1", "status": "avail", + "node": CLIENT_NAME, "ver": ver, "hash": "sha-1"} + contact_name = "test-service@" + get_host_name() + listener, port = setup_stream_listener(q, contact_name) + + announcer = AvahiAnnouncer(contact_name, "_presence._tcp", port, txt_record) + + handle = wait_for_contact_in_publish(q, bus, conn, contact_name) + + # this is the first presence, Salut connects to the contact + e = q.expect('incoming-connection', listener=listener) + incoming = e.connection + + # Salut looks up its capabilities + event = q.expect('stream-iq', connection=incoming, + query_ns=ns.DISCO_INFO) + query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] + assertEquals(CLIENT_NAME + '#' + ver, query_node.attributes['node']) + + contact_handle = conn.RequestHandles(cs.HT_CONTACT, [contact_name])[0] + + # send good reply + result = make_result_iq(event.stanza) + query = result.firstChildElement() + query['node'] = CLIENT_NAME + '#' + ver + x = query.addElement((ns.X_DATA, 'x')) + x['type'] = 'result' + + # FORM_TYPE + field = x.addElement((None, 'field')) + field['var'] = 'FORM_TYPE' + field['type'] = 'hidden' + field.addElement((None, 'value'), content='urn:ytstenut:capabilities#org.gnome.Banshee') + + # type + field = x.addElement((None, 'field')) + field['var'] = 'type' + field.addElement((None, 'value'), content='application') + + # name + field = x.addElement((None, 'field')) + field['var'] = 'name' + field.addElement((None, 'value'), content='en_GB/Banshee Media Player') + field.addElement((None, 'value'), content='fr/Banshee Lecteur de Musique') + + # capabilities + field = x.addElement((None, 'field')) + field['var'] = 'capabilities' + field.addElement((None, 'value'), content='urn:ytstenut:capabilities:yts-caps-audio') + field.addElement((None, 'value'), content='urn:ytstenut:data:jingle:rtp') + + incoming.send(result) + + # this will be fired as text channel caps will be fired + q.expect('dbus-signal', signal='ContactCapabilitiesChanged', + predicate=lambda e: contact_handle in e.args[0]) + + # add evince + tmp = banshee.copy() + tmp.update(evince) + ver = compute_caps_hash([], [], tmp) + txt_record['ver'] = ver + announcer.update(txt_record) + + # Salut looks up our capabilities + event = q.expect('stream-iq', connection=incoming, + query_ns='http://jabber.org/protocol/disco#info') + query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] + assert query_node.attributes['node'] == \ + CLIENT_NAME + '#' + txt_record['ver'] + + # send good reply + result['id'] = event.stanza['id'] + query['node'] = CLIENT_NAME + '#' + ver + + x = query.addElement((ns.X_DATA, 'x')) + x['type'] = 'result' + + # FORM_TYPE + field = x.addElement((None, 'field')) + field['var'] = 'FORM_TYPE' + field['type'] = 'hidden' + field.addElement((None, 'value'), content='urn:ytstenut:capabilities#org.gnome.Evince') + + # type + field = x.addElement((None, 'field')) + field['var'] = 'type' + field.addElement((None, 'value'), content='application') + + # name + field = x.addElement((None, 'field')) + field['var'] = 'name' + field.addElement((None, 'value'), content='en_GB/Evince Picture Viewer') + field.addElement((None, 'value'), content='fr/Evince uh, ow do you say') + + # capabilities + field = x.addElement((None, 'field')) + field['var'] = 'capabilities' + field.addElement((None, 'value'), content='urn:ytstenut:capabilities:pics') + + incoming.send(result) + + # this will be fired as text channel caps will be fired + q.expect('dbus-signal', signal='ContactCapabilitiesChanged', + predicate=lambda e: contact_handle in e.args[0]) + + # now finally ensure the sidecar + path, props = conn.Future.EnsureSidecar(ycs.STATUS_IFACE) + assertEquals({}, props) + + status = ProxyWrapper(bus.get_object(conn.bus_name, path), + ycs.STATUS_IFACE, {}) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({contact_name: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp']), + 'org.gnome.Evince': + ('application', + {'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, + ['urn:ytstenut:capabilities:pics'])} + }, discovered) + + # sweet. + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/saluttest.py b/tests/twisted/saluttest.py index 0ab6d3c..c168a77 100644 --- a/tests/twisted/saluttest.py +++ b/tests/twisted/saluttest.py @@ -204,7 +204,7 @@ def wait_for_contact_in_publish(q, bus, conn, contact_name): e = q.expect('dbus-signal', signal='MembersChangedDetailed', path=publish) for h in e.args[0]: - name = e.args[4]['member-ids'][h] + name = e.args[4]['contact-ids'][h] if name == contact_name: handle = h diff --git a/tests/twisted/tools/Makefile.am b/tests/twisted/tools/Makefile.am index 66e9265..fe7b620 100644 --- a/tests/twisted/tools/Makefile.am +++ b/tests/twisted/tools/Makefile.am @@ -2,7 +2,8 @@ service_in_files = \ org.freedesktop.Telepathy.MissionControl5.service.in \ org.freedesktop.Telepathy.Client.Logger.service.in \ - org.freedesktop.Telepathy.ConnectionManager.salut.service.in + org.freedesktop.Telepathy.ConnectionManager.salut.service.in \ + org.freedesktop.Telepathy.ConnectionManager.gabble.service.in service_files = $(service_in_files:.service.in=.service) # D-Bus config file for testing @@ -13,13 +14,15 @@ BUILT_SOURCES = \ $(service_files) \ $(conf_files) \ exec-with-log.sh \ - salut-exec-with-log.sh + salut-exec-with-log.sh \ + gabble-exec-with-log.sh EXTRA_DIST = \ $(service_in_files) \ $(conf_in_files) \ exec-with-log.sh.in \ salut-exec-with-log.sh.in \ + gabble-exec-with-log.sh.in \ fake-startup.sh \ valgrind.supp \ with-session-bus.sh \ @@ -29,4 +32,5 @@ CLEANFILES = \ $(BUILT_SOURCES) \ missioncontrol.log \ missioncontrol-*.log \ - salut-testing.log + salut-testing.log \ + gabble-testing.log diff --git a/tests/twisted/tools/gabble-exec-with-log.sh.in b/tests/twisted/tools/gabble-exec-with-log.sh.in new file mode 100644 index 0000000..48a6a43 --- /dev/null +++ b/tests/twisted/tools/gabble-exec-with-log.sh.in @@ -0,0 +1,40 @@ +#!/bin/sh + +cd "@abs_top_builddir@/tests/twisted/tools" + +export GABBLE_DEBUG=all LM_DEBUG=net GIBBER_DEBUG=all WOCKY_DEBUG=all +export GABBLE_TIMING=1 +export WOCKY_CAPS_CACHE=:memory: WOCKY_CAPS_CACHE_SIZE=50 +ulimit -c unlimited +exec >> gabble-testing.log 2>&1 + +export G_SLICE=debug-blocks + +if test -n "$GABBLE_TEST_VALGRIND"; then + export G_DEBUG=${G_DEBUG:+"${G_DEBUG},"}gc-friendly + export G_SLICE=${G_SLICE},always-malloc + export DBUS_DISABLE_MEM_POOLS=1 + GABBLE_WRAPPER="valgrind --leak-check=full --num-callers=20" + GABBLE_WRAPPER="$GABBLE_WRAPPER --show-reachable=yes" + GABBLE_WRAPPER="$GABBLE_WRAPPER --gen-suppressions=all" + GABBLE_WRAPPER="$GABBLE_WRAPPER --child-silent-after-fork=yes" + GABBLE_WRAPPER="$GABBLE_WRAPPER --suppressions=@abs_top_srcdir@/tests/suppressions/tp-glib.supp" + GABBLE_WRAPPER="$GABBLE_WRAPPER --suppressions=@abs_top_srcdir@/tests/suppressions/gabble.supp" +elif test -n "$GABBLE_TEST_REFDBG"; then + if test -z "$REFDBG_OPTIONS" ; then + export REFDBG_OPTIONS="btnum=10" + fi + if test -z "$GABBLE_WRAPPER" ; then + GABBLE_WRAPPER="refdbg" + fi +elif test -n "$GABBLE_TEST_STRACE"; then + GABBLE_WRAPPER="strace -o strace.log" +elif test -n "$GABBLE_TEST_BACKTRACE"; then + GABBLE_WRAPPER="gdb -x run_and_bt.gdb" +fi + +# Prevent libproxy from hitting the network for wpad configuration +export PX_MODULE_BLACKLIST=config_wpad + +export G_DEBUG=fatal-warnings,fatal-criticals" ${G_DEBUG}" +exec @abs_top_builddir@/libtool --mode=execute $GABBLE_WRAPPER @GABBLE_EXECUTABLE@ diff --git a/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service.in b/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service.in new file mode 100644 index 0000000..3e3c58d --- /dev/null +++ b/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.freedesktop.Telepathy.ConnectionManager.gabble +Exec=/bin/sh @abs_top_builddir@/tests/twisted/tools/gabble-exec-with-log.sh diff --git a/tests/twisted/yconstants.py b/tests/twisted/yconstants.py index 4f965d9..4a69cb5 100644 --- a/tests/twisted/yconstants.py +++ b/tests/twisted/yconstants.py @@ -23,3 +23,4 @@ ERROR_TYPE_WAIT = 5 MESSAGE_NS = 'urn:ytstenut:message' STATUS_NS = 'urn:ytstenut:status' +SERVICE_NS = 'urn:ytstenut:service' |