diff options
Diffstat (limited to 'tests/twisted')
32 files changed, 4744 insertions, 0 deletions
diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am new file mode 100644 index 0000000..9cbc256 --- /dev/null +++ b/tests/twisted/Makefile.am @@ -0,0 +1,130 @@ +TWISTED_TESTS = \ + avatar-requirements.py \ + caps_helper.py \ + gateways.py \ + text/initiate.py \ + text/initiate-requestotron.py \ + text/destroy.py \ + text/ensure.py \ + text/respawn.py \ + text/send-error.py \ + text/send-to-correct-resource.py \ + text/test-chat-state.py \ + text/test-text-delayed.py \ + text/test-text-no-body.py \ + text/test-text.py \ + test-debug.py \ + test-disco.py \ + test-disco-no-reply.py \ + test-fallback-socks5-proxy.py \ + test-register.py \ + test-set-status-idempotence.py \ + test-location.py \ + pubsub.py \ + sidecars.py + +TESTS = + +TESTS_ENVIRONMENT = \ + PYTHONPATH=@abs_top_srcdir@/tests/twisted:@abs_top_builddir@/tests/twisted + +check-local: check-coding-style check-twisted + +# set to 6 when using refdbg, to give Gabble time to exit +CHECK_TWISTED_SLEEP=0 + +check-twisted: + $(MAKE) -C tools + rm -f tools/core + rm -f tools/vgcore.* + rm -f tools/gabble-testing.log + rm -f tools/strace.log + if test -n "$$GABBLE_TEST_REFDBG"; then \ + sleep=6; \ + else \ + sleep=$(CHECK_TWISTED_SLEEP); \ + fi; \ + sh $(srcdir)/tools/with-session-bus.sh \ + --sleep=$$sleep \ + --config-file=tools/tmp-session-bus.conf \ + -- $(MAKE) check-TESTS \ + TESTS="$(TWISTED_TESTS)" \ + TESTS_ENVIRONMENT="$(TESTS_ENVIRONMENT) $(TEST_PYTHON) -u" + @if test -e tools/core; then\ + echo "Core dump exists: tools/core";\ + exit 1;\ + fi + +if ENABLE_DEBUG +DEBUGGING_PYBOOL = True +else +DEBUGGING_PYBOOL = False +endif + +if ENABLE_PLUGINS +PLUGINS_ENABLED_PYBOOL = True +else +PLUGINS_ENABLED_PYBOOL = False +endif + +if ENABLE_CHANNEL_TYPE_CALL +CHANNEL_TYPE_CALL_ENABLED_PYBOOL = True +else +CHANNEL_TYPE_CALL_ENABLED_PYBOOL = False +endif + +config.py: Makefile + $(AM_V_GEN) { \ + echo "PACKAGE_STRING = \"$(PACKAGE_STRING)\""; \ + echo "DEBUGGING = $(DEBUGGING_PYBOOL)"; \ + echo "PLUGINS_ENABLED = $(PLUGINS_ENABLED_PYBOOL)"; \ + echo "CHANNEL_TYPE_CALL_ENABLED = $(CHANNEL_TYPE_CALL_ENABLED_PYBOOL)"; \ + } > $@ + +BUILT_SOURCES = config.py + +EXTRA_DIST = \ + $(TWISTED_TESTS) \ + bytestream.py \ + constants.py \ + gabbletest.py \ + httptest.py \ + servicetest.py \ + jingle/jingletest.py \ + jingle/jingletest2.py \ + mucutil.py \ + ns.py \ + olpc/util.py \ + tubes/muctubeutil.py \ + tubes/tubetestutil.py \ + search/search_helper.py \ + file-transfer/file_transfer_helper.py + +noinst_PROGRAMS = \ + telepathy-gabble-debug + +telepathy_gabble_debug_SOURCES = \ + main-debug.c \ + test-resolver.c \ + test-resolver.h + +telepathy_gabble_debug_LDADD = \ + $(top_builddir)/src/libgabble-convenience.la \ + $(ALL_LIBS) + +telepathy_gabble_debug_LDFLAGS = -export-dynamic + +AM_CFLAGS = $(ERROR_CFLAGS) @DBUS_CFLAGS@ @GLIB_CFLAGS@ @WOCKY_CFLAGS@ \ + @TP_GLIB_CFLAGS@ \ + -I $(top_srcdir)/src -I $(top_builddir)/src \ + -I $(top_srcdir)/lib -I $(top_builddir)/lib \ + -I $(top_srcdir) -I $(top_builddir) +ALL_LIBS = @DBUS_LIBS@ @GLIB_LIBS@ @WOCKY_LIBS@ @TP_GLIB_LIBS@ + +CLEANFILES = gabble-[1-9]*.log *.pyc */*.pyc config.py + +check_misc_sources = $(TESTS) + +include $(top_srcdir)/tools/check-coding-style.mk + +SUBDIRS = tools diff --git a/tests/twisted/bytestream.py b/tests/twisted/bytestream.py new file mode 100644 index 0000000..54ef9e6 --- /dev/null +++ b/tests/twisted/bytestream.py @@ -0,0 +1,882 @@ +import base64 +import hashlib +import sys +import random +import socket + +from twisted.internet.protocol import Factory, Protocol +from twisted.internet import reactor +from twisted.words.protocols.jabber.client import IQ +from twisted.words.xish import xpath, domish +from twisted.internet.error import CannotListenError + +from servicetest import Event, EventPattern +from gabbletest import acknowledge_iq, make_result_iq, elem_iq, elem +import ns + +def wait_events(q, expected, my_event): + tmp = expected + [my_event] + events = q.expect_many(*tmp) + return events[:-1], events[-1] + +def create_from_si_offer(stream, q, bytestream_cls, iq, initiator): + si_nodes = xpath.queryForNodes('/iq/si', iq) + assert si_nodes is not None + assert len(si_nodes) == 1 + si = si_nodes[0] + + feature = xpath.queryForNodes('/si/feature', si)[0] + x = xpath.queryForNodes('/feature/x', feature)[0] + assert x['type'] == 'form' + field = xpath.queryForNodes('/x/field', x)[0] + assert field['var'] == 'stream-method' + assert field['type'] == 'list-single' + + bytestreams = [] + for value in xpath.queryForNodes('/field/option/value', field): + bytestreams.append(str(value)) + + bytestream = bytestream_cls(stream, q, si['id'], initiator, + iq['to'], False) + + bytestream.check_si_offer(iq, bytestreams) + + return bytestream, si['profile'] + +def is_ipv4(address): + try: + socket.inet_pton(socket.AF_INET, address) + except (ValueError, socket.error): + return False + return True + +class Bytestream(object): + def __init__(self, stream, q, sid, initiator, target, initiated): + self.stream = stream + self.q = q + + self.stream_id = sid + self.initiator = initiator + self.target = target + self.initiated = initiated + + def open_bytestream(self, expected_before=[], expected_after=[]): + raise NotImplemented + + def send_data(self, data): + raise NotImplemented + + def get_ns(self): + raise NotImplemented + + def wait_bytestream_open(self): + raise NotImplemented + + def get_data(self, size=0): + raise NotImplemented + + def wait_bytestream_closed(self, expected=[]): + raise NotImplemented + + def check_si_offer(self, iq, bytestreams): + assert self.get_ns() in bytestreams + + def close(self): + raise NotImplemented + +##### XEP-0095: Stream Initiation ##### + + def _create_si_offer(self, profile, to=None): + assert self.initiated + + iq = IQ(self.stream, 'set') + iq['from'] = self.initiator + if to is None: + iq['to'] = self.target + else: + iq['to'] = to + si = iq.addElement((ns.SI, 'si')) + si['id'] = self.stream_id + si['profile'] = profile + feature = si.addElement((ns.FEATURE_NEG, 'feature')) + x = feature.addElement((ns.X_DATA, 'x')) + x['type'] = 'form' + field = x.addElement((None, 'field')) + field['var'] = 'stream-method' + field['type'] = 'list-single' + + return iq, si, field + + def create_si_offer(self, profile, to=None): + iq, si, field = self._create_si_offer(profile, to) + option = field.addElement((None, 'option')) + value = option.addElement((None, 'value')) + value.addContent(self.get_ns()) + + return iq, si + + def create_si_reply(self, iq, to=None): + result = make_result_iq(self.stream, iq) + result['from'] = iq['to'] + if to is None: + result['to'] = self.initiator + else: + result['to'] = to + res_si = result.firstChildElement() + res_feature = res_si.addElement((ns.FEATURE_NEG, 'feature')) + res_x = res_feature.addElement((ns.X_DATA, 'x')) + res_x['type'] = 'submit' + res_field = res_x.addElement((None, 'field')) + res_field['var'] = 'stream-method' + res_value = res_field.addElement((None, 'value')) + res_value.addContent(self.get_ns()) + + return result, res_si + + def check_si_reply(self, iq): + si = xpath.queryForNodes('/iq/si[@xmlns="%s"]' % ns.SI, + iq)[0] + value = xpath.queryForNodes('/si/feature/x/field/value', si) + assert len(value) == 1 + proto = value[0] + assert str(proto) == self.get_ns() + +##### XEP-0065: SOCKS5 Bytestreams ##### +def listen_socks5(q): + for port in range(5000, 5100): + try: + reactor.listenTCP(port, S5BFactory(q.append)) + except CannotListenError: + continue + else: + return port + + assert False, "Can't find a free port" + +def announce_socks5_proxy(q, stream, disco_stanza): + reply = make_result_iq(stream, disco_stanza) + query = xpath.queryForNodes('/iq/query', reply)[0] + item = query.addElement((None, 'item')) + item['jid'] = 'proxy.localhost' + stream.send(reply) + + # wait for proxy disco#info query + event = q.expect('stream-iq', to='proxy.localhost', query_ns=ns.DISCO_INFO, + iq_type='get') + + reply = elem_iq(stream, 'result', from_='proxy.localhost', id=event.stanza['id'])( + elem(ns.DISCO_INFO, 'query')( + elem('identity', category='proxy', type='bytestreams', name='SOCKS5 Bytestreams')(), + elem('feature', var=ns.BYTESTREAMS)())) + stream.send(reply) + + # Gabble asks for SOCKS5 info + event = q.expect('stream-iq', to='proxy.localhost', query_ns=ns.BYTESTREAMS, + iq_type='get') + + port = listen_socks5(q) + reply = elem_iq(stream, 'result', id=event.stanza['id'], from_='proxy.localhost')( + elem(ns.BYTESTREAMS, 'query')( + elem('streamhost', jid='proxy.localhost', host='127.0.0.1', port=str(port))())) + stream.send(reply) + +class BytestreamS5B(Bytestream): + def __init__(self, stream, q, sid, initiator, target, initiated): + Bytestream.__init__(self, stream, q, sid, initiator, target, initiated) + + # hosts that will be announced when sending S5B open IQ + self.hosts = [ + # Not working streamhost + ('invalid.invalid', 'invalid.invalid'), + # Working streamhost + (self.initiator, '127.0.0.1'), + # This works too but should not be tried as Gabble should just + # connect to the previous one + ('Not me', '127.0.0.1')] + + def get_ns(self): + return ns.BYTESTREAMS + + def _send_socks5_init(self, port): + iq = IQ(self.stream, 'set') + iq['to'] = self.target + iq['from'] = self.initiator + query = iq.addElement((ns.BYTESTREAMS, 'query')) + query['sid'] = self.stream_id + query['mode'] = 'tcp' + for jid, host in self.hosts: + streamhost = query.addElement('streamhost') + streamhost['jid'] = jid + streamhost['host'] = host + streamhost['port'] = str(port) + self.stream.send(iq) + + def _wait_auth_request(self): + event = self.q.expect('s5b-data-received') + assert event.data == '\x05\x01\x00' # version 5, 1 auth method, no auth + self.transport = event.transport + + def _send_auth_reply(self): + self.transport.write('\x05\x00') # version 5, no auth + + def _compute_hash_domain(self): + # sha-1(sid + initiator + target) + unhashed_domain = self.stream_id + self.initiator + self.target + return hashlib.sha1(unhashed_domain).hexdigest() + + def _wait_connect_cmd(self): + event = self.q.expect('s5b-data-received', transport=self.transport) + # version 5, connect, reserved, domain type + expected_connect = '\x05\x01\x00\x03' + expected_connect += chr(40) # len (SHA-1) + expected_connect += self._compute_hash_domain() + expected_connect += '\x00\x00' # port + assert event.data == expected_connect + + def _send_connect_reply(self): + connect_reply = '\x05\x00\x00\x03' + connect_reply += chr(40) # len (SHA-1) + connect_reply += self._compute_hash_domain() + connect_reply += '\x00\x00' # port + self.transport.write(connect_reply) + + def _check_s5b_reply(self, iq): + streamhost = xpath.queryForNodes('/iq/query/streamhost-used', iq)[0] + assert streamhost['jid'] == self.initiator + + def _socks5_expect_connection(self, expected_before, expected_after): + events_before, _ = wait_events(self.q, expected_before, + EventPattern('s5b-connected')) + + self._wait_auth_request() + self._send_auth_reply() + self._wait_connect_cmd() + self._send_connect_reply() + + # wait for S5B IQ reply + events_after, e = wait_events(self.q, expected_after, + EventPattern('stream-iq', iq_type='result', to=self.initiator)) + + self._check_s5b_reply(e.stanza) + + return events_before, events_after + + def open_bytestream(self, expected_before=[], expected_after=[]): + port = listen_socks5(self.q) + + self._send_socks5_init(port) + return self._socks5_expect_connection(expected_before, expected_after) + + def send_data(self, data): + self.transport.write(data) + + def _expect_socks5_init(self): + event = self.q.expect('stream-iq', iq_type='set') + iq = event.stanza + query = xpath.queryForNodes('/iq/query', iq)[0] + assert query.uri == ns.BYTESTREAMS + + mode = query['mode'] + sid = query['sid'] + hosts = [] + + for streamhost in xpath.queryForNodes('/query/streamhost', query): + hosts.append((streamhost['jid'], streamhost['host'], int(streamhost['port']))) + return iq['id'], mode, sid, hosts + + def _send_auth_cmd(self): + #version 5, 1 auth method, no auth + self.transport.write('\x05\x01\x00') + + def _wait_auth_reply(self): + event = self.q.expect('s5b-data-received') + assert event.data == '\x05\x00' # version 5, no auth + + def _send_connect_cmd(self): + # version 5, connect, reserved, domain type + connect = '\x05\x01\x00\x03' + connect += chr(40) # len (SHA-1) + connect += self._compute_hash_domain() + connect += '\x00\x00' # port + self.transport.write(connect) + + def _wait_connect_reply(self): + event = self.q.expect('s5b-data-received') + # version 5, succeed, reserved, domain type + expected_reply = '\x05\x00\x00\x03' + expected_reply += chr(40) # len (SHA-1) + expected_reply += self._compute_hash_domain() + expected_reply += '\x00\x00' # port + assert event.data == expected_reply + + def _socks5_connect(self, host, port): + reactor.connectTCP(host, port, S5BFactory(self.q.append)) + + event = self.q.expect('s5b-connected') + self.transport = event.transport + + self._send_auth_cmd() + self._wait_auth_reply() + self._send_connect_cmd() + self._wait_connect_reply() + return True + + def _send_socks5_reply(self, id, stream_used): + result = IQ(self.stream, 'result') + result['id'] = id + result['from'] = self.target + result['to'] = self.initiator + query = result.addElement((ns.BYTESTREAMS, 'query')) + streamhost_used = query.addElement((None, 'streamhost-used')) + streamhost_used['jid'] = stream_used + result.send() + + def wait_bytestream_open(self): + id, mode, sid, hosts = self._expect_socks5_init() + + assert mode == 'tcp' + assert sid == self.stream_id + + stream_host_found = False + + for jid, host, port in hosts: + if not is_ipv4(host): + continue + + if jid == self.initiator: + stream_host_found = True + if self._socks5_connect(host, port): + self._send_socks5_reply(id, jid) + else: + # Connection failed + self.send_not_found(id) + break + assert stream_host_found + + def get_data(self, size=0): + binary = '' + received = False + while not received: + e = self.q.expect('s5b-data-received', transport=self.transport) + binary += e.data + + if len(binary) >= size or size == 0: + received = True + + return binary + + def wait_bytestream_closed(self, expected=[]): + events, _ = wait_events(self.q, expected, + EventPattern('s5b-connection-lost')) + return events + + def check_error_stanza(self, iq): + error = xpath.queryForNodes('/iq/error', iq)[0] + assert error['code'] == '404' + assert error['type'] == 'cancel' + + def send_not_found(self, id): + iq = IQ(self.stream, 'error') + iq['to'] = self.initiator + iq['from'] = self.target + iq['id'] = id + error = iq.addElement(('', 'error')) + error['type'] = 'cancel' + error['code'] = '404' + self.stream.send(iq) + + def close(self): + self.transport.loseConnection() + +class BytestreamS5BPidgin(BytestreamS5B): + """Simulate buggy S5B implementation (as Pidgin's one)""" + def _send_connect_reply(self): + # version 5, ok, reserved, domain type + connect_reply = '\x05\x00\x00\x03' + # I'm Pidgin, why should I respect SOCKS5 XEP? + domain = '127.0.0.1' + connect_reply += chr(len(domain)) + connect_reply += domain + connect_reply += '\x00\x00' # port + self.transport.write(connect_reply) + +class BytestreamS5BCannotConnect(BytestreamS5B): + """SOCKS5 bytestream not working because target can't connect + to initiator.""" + def __init__(self, stream, q, sid, initiator, target, initiated): + BytestreamS5B.__init__(self, stream, q, sid, initiator, target, initiated) + + self.hosts = [('invalid.invalid', 'invalid.invalid')] + + def open_bytestream(self, expected_before=[], expected_after=[]): + self._send_socks5_init(12345) + + events_before, iq_event = wait_events(self.q, expected_before, + EventPattern('stream-iq', iq_type='error', to=self.initiator)) + + self.check_error_stanza(iq_event.stanza) + + return events_before, [] + + def _socks5_connect(self, host, port): + # Pretend we can't connect to it + return False + +class BytestreamS5BWrongHash(BytestreamS5B): + """Connection is closed because target sends the wrong hash""" + def __init__(self, stream, q, sid, initiator, target, initiated): + BytestreamS5B.__init__(self, stream, q, sid, initiator, target, initiated) + + self.hosts = [(self.initiator, '127.0.0.1')] + + def _send_connect_cmd(self): + # version 5, connect, reserved, domain type + connect = '\x05\x01\x00\x03' + # send wrong hash as domain + domain = 'this is wrong' + connect += chr(len(domain)) + connect += domain + connect += '\x00\x00' # port + self.transport.write(connect) + + def _socks5_connect(self, host, port): + reactor.connectTCP(host, port, S5BFactory(self.q.append)) + + event = self.q.expect('s5b-connected') + self.transport = event.transport + + self._send_auth_cmd() + self._wait_auth_reply() + self._send_connect_cmd() + + # Gabble disconnects the connection because we sent a wrong hash + self.q.expect('s5b-connection-lost') + return False + + def _socks5_expect_connection(self, expected_before, expected_after): + events_before, _ = wait_events(self.q, expected_before, + EventPattern('s5b-connected')) + + self._wait_auth_request() + self._send_auth_reply() + self._wait_connect_cmd() + + # pretend the hash was wrong and close the transport + self.transport.loseConnection() + + iq_event = self.q.expect('stream-iq', iq_type='error', to=self.initiator) + self.check_error_stanza(iq_event.stanza) + + return events_before, [] + +class BytestreamS5BRelay(BytestreamS5B): + """Direct connection doesn't work so we use a relay""" + def __init__(self, stream, q, sid, initiator, target, initiated): + BytestreamS5B.__init__(self, stream, q, sid, initiator, target, initiated) + + self.hosts = [(self.initiator, 'invalid.invalid'), + ('proxy.localhost', '127.0.0.1')] + + # This is the only thing we need to check to test the Target side as the + # protocol is similar from this side. + def _check_s5b_reply(self, iq): + streamhost = xpath.queryForNodes('/iq/query/streamhost-used', iq)[0] + assert streamhost['jid'] == 'proxy.localhost' + + def wait_bytestream_open(self): + # The only difference of using a relay on the Target side is to + # connect to another streamhost. + id, mode, sid, hosts = self._expect_socks5_init() + + assert mode == 'tcp' + assert sid == self.stream_id + + proxy_found = False + + for jid, host, port in hosts: + if jid != self.initiator: + proxy_found = True + # connect to the (fake) relay + if self._socks5_connect(host, port): + self._send_socks5_reply(id, jid) + else: + assert False + break + assert proxy_found + + # The initiator (Gabble) is now supposed to connect to the proxy too + self._wait_connect_to_proxy() + + def _wait_connect_to_proxy(self): + e = self.q.expect('s5b-connected') + self.transport = e.transport + + self._wait_auth_request() + self._send_auth_reply() + self._wait_connect_cmd() + self._send_connect_reply() + + self._wait_activation_iq() + + def _wait_activation_iq(self): + e = self.q.expect('stream-iq', iq_type='set', to='proxy.localhost', + query_ns=ns.BYTESTREAMS) + + query = xpath.queryForNodes('/iq/query', e.stanza)[0] + assert query['sid'] == self.stream_id + activate = xpath.queryForNodes('/iq/query/activate', e.stanza)[0] + assert str(activate) == self.target + + self._reply_activation_iq(e.stanza) + + def _reply_activation_iq(self, iq): + reply = make_result_iq(self.stream, iq) + reply.send() + + def _socks5_connect(self, host, port): + # No point to emulate the proxy. Just pretend the Target properly + # connects, auth and requests connection + return True + + def wait_bytestream_closed(self, expected=[]): + if expected == []: + return [] + + return self.q.expect_many(*expected) + + +class BytestreamS5BRelayBugged(BytestreamS5BRelay): + """Simulate bugged ejabberd (< 2.0.2) proxy sending wrong CONNECT reply""" + def _send_connect_reply(self): + # send a 6 bytes wrong reply + connect_reply = '\x05\x00\x00\x00\x00\x00' + self.transport.write(connect_reply) + +class S5BProtocol(Protocol): + def connectionMade(self): + self.factory.event_func(Event('s5b-connected', + transport=self.transport)) + + def dataReceived(self, data): + self.factory.event_func(Event('s5b-data-received', data=data, + transport=self.transport)) + +class S5BFactory(Factory): + protocol = S5BProtocol + + def __init__(self, event_func): + self.event_func = event_func + + def buildProtocol(self, addr): + protocol = Factory.buildProtocol(self, addr) + return protocol + + def startedConnecting(self, connector): + self.event_func(Event('s5b-started-connecting', connector=connector)) + + def clientConnectionLost(self, connector, reason): + self.event_func(Event('s5b-connection-lost', connector=connector, + reason=reason)) + + def clientConnectionFailed(self, connector, reason): + self.event_func(Event('s5b-connection-failed', reason=reason)) + +def expect_socks5_reply(q): + event = q.expect('stream-iq', iq_type='result') + iq = event.stanza + query = xpath.queryForNodes('/iq/query', iq)[0] + assert query.uri == ns.BYTESTREAMS + streamhost_used = xpath.queryForNodes('/query/streamhost-used', query)[0] + return streamhost_used + +##### XEP-0047: In-Band Bytestreams (IBB) ##### + +class BytestreamIBB(Bytestream): + def __init__(self, stream, q, sid, initiator, target, initiated): + Bytestream.__init__(self, stream, q, sid, initiator, target, initiated) + + self.seq = 0 + self.checked = False + + def get_ns(self): + return ns.IBB + + def check_si_reply(self, iq): + self.checked = True + + def open_bytestream(self, expected_before=[], expected_after=[]): + # open IBB bytestream + iq = IQ(self.stream, 'set') + iq['to'] = self.target + iq['from'] = self.initiator + open = iq.addElement((ns.IBB, 'open')) + open['sid'] = self.stream_id + # set a ridiculously small block size to stress test IBB buffering + open['block-size'] = '1' + + assert self.checked + + events_before = self.q.expect_many(*expected_before) + self.stream.send(iq) + events_after = self.q.expect_many(*expected_after) + + return events_before, events_after + + def _send(self, from_, to, data): + raise NotImplemented + + def send_data(self, data): + if self.initiated: + from_ = self.initiator + to = self.target + else: + from_ = self.target + to = self.initiator + + self._send(from_, to, data) + self.seq += 1 + + def wait_bytestream_open(self): + # Wait IBB open iq + event = self.q.expect('stream-iq', iq_type='set') + open = xpath.queryForNodes('/iq/open', event.stanza)[0] + assert open.uri == ns.IBB + assert open['sid'] == self.stream_id + + # open IBB bytestream + acknowledge_iq(self.stream, event.stanza) + + def get_data(self, size=0): + # wait for IBB stanza. Gabble always uses IQ + + binary = '' + received = False + while not received: + ibb_event = self.q.expect('stream-iq', query_ns=ns.IBB) + + data_nodes = xpath.queryForNodes('/iq/data[@xmlns="%s"]' % ns.IBB, + ibb_event.stanza) + assert data_nodes is not None + assert len(data_nodes) == 1 + ibb_data = data_nodes[0] + binary += base64.b64decode(str(ibb_data)) + + assert ibb_data['sid'] == self.stream_id + + # ack the IQ + result = make_result_iq(self.stream, ibb_event.stanza) + result.send() + + if len(binary) >= size or size == 0: + received = True + + return binary + + def wait_bytestream_closed(self, expected=[]): + events, close_event = wait_events(self.q, expected, + EventPattern('stream-iq', iq_type='set', query_name='close', query_ns=ns.IBB)) + + # sender finish to send the file and so close the bytestream + acknowledge_iq(self.stream, close_event.stanza) + return events + + def close(self): + if self.initiated: + from_ = self.initiator + to = self.target + else: + from_ = self.target + to = self.initiator + + iq = elem_iq(self.stream, 'set', from_=from_, to=to, id=str(id))( + elem('close', xmlns=ns.IBB, sid=self.stream_id)()) + + self.stream.send(iq) + +class BytestreamIBBMsg(BytestreamIBB): + def _send(self, from_, to, data): + message = domish.Element(('jabber:client', 'message')) + message['to'] = to + message['from'] = from_ + data_node = message.addElement((ns.IBB, 'data')) + data_node['sid'] = self.stream_id + data_node['seq'] = str(self.seq) + data_node.addContent(base64.b64encode(data)) + self.stream.send(message) + + def _wait_data_event(self): + ibb_event = self.q.expect('stream-message') + + data_nodes = xpath.queryForNodes('/message/data[@xmlns="%s"]' % ns.IBB, + ibb_event.stanza) + assert data_nodes is not None + assert len(data_nodes) == 1 + ibb_data = data_nodes[0] + assert ibb_data['sid'] == self.stream_id + return str(ibb_data), ibb_data['sid'] + +class BytestreamIBBIQ(BytestreamIBB): + def _send(self, from_, to, data): + id = random.randint(0, sys.maxint) + + iq = elem_iq(self.stream, 'set', from_=from_, to=to, id=str(id))( + elem('data', xmlns=ns.IBB, sid=self.stream_id, seq=str(self.seq))( + (unicode(base64.b64encode(data))))) + + self.stream.send(iq) + +##### SI Fallback (Gabble specific extension) ##### +class BytestreamSIFallback(Bytestream): + """Abstract class used for all the SI fallback scenarios""" + def __init__(self, stream, q, sid, initiator, target, initiated): + Bytestream.__init__(self, stream, q, sid, initiator, target, initiated) + + self.socks5 = BytestreamS5B(stream, q, sid, initiator, target, + initiated) + + self.ibb = BytestreamIBBMsg(stream, q, sid, initiator, target, + initiated) + + def create_si_offer(self, profile, to=None): + iq, si, field = self._create_si_offer(profile, to) + + # add SOCKS5 + option = field.addElement((None, 'option')) + value = option.addElement((None, 'value')) + value.addContent(self.socks5.get_ns()) + # add IBB + option = field.addElement((None, 'option')) + value = option.addElement((None, 'value')) + value.addContent(self.ibb.get_ns()) + + si_multiple = si.addElement((ns.SI_MULTIPLE, 'si-multiple')) + + return iq, si + + def check_si_reply(self, iq): + value = xpath.queryForNodes( + '/iq/si[@xmlns="%s"]/si-multiple[@xmlns="%s"]/value' % + (ns.SI, ns.SI_MULTIPLE), iq) + assert len(value) == 2 + assert str(value[0]) == self.socks5.get_ns() + assert str(value[1]) == self.ibb.get_ns() + + def create_si_reply(self, iq, to=None): + result = make_result_iq(self.stream, iq) + result['from'] = iq['to'] + if to is None: + result['to'] = self.initiator + else: + result['to'] = to + res_si = result.firstChildElement() + si_multiple = res_si.addElement((ns.SI_MULTIPLE, 'si-multiple')) + # add SOCKS5 + res_value = si_multiple.addElement((None, 'value')) + res_value.addContent(self.socks5.get_ns()) + # add IBB + res_value = si_multiple.addElement((None, 'value')) + res_value.addContent(self.ibb.get_ns()) + + return result, res_si + + def open_bytestream(self, expected_before=[], expected_after=[]): + # first propose to peer to connect using SOCKS5 + # We set an invalid IP so that won't work + self.socks5._send_socks5_init([ + # Not working streamhost + (self.initiator, 'invalid.invalid', 12345), + ]) + + events_before, iq_event = wait_events(self.q, expected_before, + EventPattern('stream-iq', iq_type='error', to=self.initiator)) + + self.socks5.check_error_stanza(iq_event.stanza) + + # socks5 failed, let's try IBB + _, events_after = self.ibb.open_bytestream([], expected_after) + + return events_before, events_after + + def send_data(self, data): + self.used.send_data(data) + + def get_data(self, size=0): + return self.used.get_data(size) + + def wait_bytestream_closed(self, expected=[]): + return self.used.wait_bytestream_closed(expected) + + def check_si_offer(self, iq, bytestreams): + assert self.socks5.get_ns() in bytestreams + assert self.ibb.get_ns() in bytestreams + + # check if si-multiple is supported + si_multiple = xpath.queryForNodes( + '/iq/si[@xmlns="%s"]/si-multiple[@xmlns="%s"]' + % (ns.SI, ns.SI_MULTIPLE), iq) + assert si_multiple is not None + + def close(self): + return self.used.close() + +class BytestreamSIFallbackS5CannotConnect(BytestreamSIFallback): + """Try to use SOCKS5 and fallback to IBB because the target can't connect + to the receiver.""" + def __init__(self, stream, q, sid, initiator, target, initiated): + BytestreamSIFallback.__init__(self, stream, q, sid, initiator, target, initiated) + + self.socks5 = BytestreamS5BCannotConnect(stream, q, sid, initiator, target, + initiated) + + self.used = self.ibb + + def open_bytestream(self, expected_before=[], expected_after=[]): + # First propose to peer to connect using SOCKS5 + # That won't work as target can't connect + events_before, _ = self.socks5.open_bytestream(expected_before) + + # socks5 failed, let's try IBB + _, events_after = self.ibb.open_bytestream([], expected_after) + + return events_before, events_after + + def wait_bytestream_open(self): + # Gabble tries SOCKS5 first + self.socks5.wait_bytestream_open() + + # Gabble now tries IBB + self.ibb.wait_bytestream_open() + + def check_si_reply (self, iq): + self.ibb.check_si_reply (iq) + +class BytestreamSIFallbackS5WrongHash(BytestreamSIFallback): + """Try to use SOCKS5 and fallback to IBB because target sent the wrong hash + as domain in the CONNECT command.""" + def __init__(self, stream, q, sid, initiator, target, initiated): + BytestreamSIFallback.__init__(self, stream, q, sid, initiator, target, initiated) + + self.socks5 = BytestreamS5BWrongHash(stream, q, sid, initiator, target, + initiated) + + self.used = self.ibb + + def open_bytestream(self, expected_before=[], expected_after=[]): + # SOCKS5 won't work because we'll pretend the hash was wrong and + # close the connection + events_before, _ = self.socks5.open_bytestream(expected_before) + + # socks5 failed, let's try IBB + _, events_after = self.ibb.open_bytestream([], expected_after) + return events_before, events_after + + def wait_bytestream_open(self): + # BytestreamS5BWrongHash will send a wrong hash so Gabble will + # disconnect the connection + self.socks5.wait_bytestream_open() + + # Gabble now tries IBB + self.ibb.wait_bytestream_open() + + def check_si_reply (self, iq): + self.ibb.check_si_reply (iq) diff --git a/tests/twisted/constants.py b/tests/twisted/constants.py new file mode 100644 index 0000000..f80e7dd --- /dev/null +++ b/tests/twisted/constants.py @@ -0,0 +1,306 @@ +""" +Some handy constants for other tests to share and enjoy. +""" + +from dbus import PROPERTIES_IFACE + +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_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_TYPE_CALL = CHANNEL + ".Type.Call.DRAFT" +CHANNEL_TYPE_CONTACT_LIST = CHANNEL + ".Type.ContactList" +CHANNEL_TYPE_CONTACT_SEARCH = CHANNEL + ".Type.ContactSearch.DRAFT2" +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" + +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_VIDEO = CHANNEL_TYPE_CALL + '.InitialVideo' +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_STREAM_TRANSPORT_RAW_UDP = 0 +CALL_STREAM_TRANSPORT_ICE = 1 +CALL_STREAM_TRANSPORT_GOOGLE = 2 + +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_EARLY_MEDIA = 1 +CALL_DISPOSITION_INITIAL = 2 + +CALL_SENDING_STATE_NONE = 0 +CALL_SENDING_STATE_PENDING_SEND = 1 +CALL_SENDING_STATE_SENDING = 2 + +CONN = "org.freedesktop.Telepathy.Connection" +CONN_IFACE_AVATARS = CONN + '.Interface.Avatars' +CONN_IFACE_CAPS = CONN + '.Interface.Capabilities' +CONN_IFACE_CONTACTS = CONN + '.Interface.Contacts' +CONN_IFACE_CONTACT_CAPS = CONN + '.Interface.ContactCapabilities' +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' + +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' + +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' + +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 + +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' + +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 diff --git a/tests/twisted/gabbletest.py b/tests/twisted/gabbletest.py new file mode 100644 index 0000000..d6e4c34 --- /dev/null +++ b/tests/twisted/gabbletest.py @@ -0,0 +1,606 @@ + +""" +Infrastructure code for testing Gabble by pretending to be a Jabber server. +""" + +import base64 +import os +import hashlib +import sys +import random +import re +import traceback + +import ns +import constants as cs +import servicetest +from servicetest import ( + assertEquals, assertLength, assertContains, wrap_channel, EventPattern, call_async + ) +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 + +import dbus + +NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl' +NS_XMPP_BIND = 'urn:ietf:params:xml:ns:xmpp-bind' + +def make_result_iq(stream, iq): + 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: + result.addElement((query.uri, query.name)) + + return result + +def acknowledge_iq(stream, iq): + stream.send(make_result_iq(stream, iq)) + +def send_error_reply(stream, iq, error_stanza=None): + result = IQ(stream, "error") + result["id"] = iq["id"] + query = iq.firstChildElement() + to = iq.getAttribute('to') + if to is not None: + result["from"] = to + + if query: + result.addElement((query.uri, query.name)) + + if error_stanza: + result.addChild(error_stanza) + + stream.send(result) + +def request_muc_handle(q, conn, stream, muc_jid): + servicetest.call_async(q, conn, 'RequestHandles', 2, [muc_jid]) + event = q.expect('dbus-return', method='RequestHandles') + return event.value[0][0] + +def make_muc_presence(affiliation, role, muc_jid, alias, jid=None): + presence = domish.Element((None, 'presence')) + presence['from'] = '%s/%s' % (muc_jid, alias) + x = presence.addElement((ns.MUC_USER, 'x')) + item = x.addElement('item') + item['affiliation'] = affiliation + item['role'] = role + if jid is not None: + item['jid'] = jid + return presence + +def sync_stream(q, stream): + """Used to ensure that Gabble has processed all stanzas sent to it.""" + + iq = IQ(stream, "get") + id = iq['id'] + iq.addElement(('http://jabber.org/protocol/disco#info', 'query')) + stream.send(iq) + q.expect('stream-iq', query_ns='http://jabber.org/protocol/disco#info', + predicate=(lambda event: + event.stanza['id'] == id and event.iq_type == 'result')) + +class JabberAuthenticator(xmlstream.Authenticator): + "Trivial XML stream authenticator that accepts one username/digest pair." + + def __init__(self, username, password, resource=None): + self.username = username + self.password = password + self.resource = resource + xmlstream.Authenticator.__init__(self) + + # Patch in fix from http://twistedmatrix.com/trac/changeset/23418. + # This monkeypatch taken from Gadget source code + from twisted.words.xish.utility import EventDispatcher + + def _addObserver(self, onetime, event, observerfn, priority, *args, + **kwargs): + if self._dispatchDepth > 0: + self._updateQueue.append(lambda: self._addObserver(onetime, event, + observerfn, priority, *args, **kwargs)) + + return self._oldAddObserver(onetime, event, observerfn, priority, + *args, **kwargs) + + EventDispatcher._oldAddObserver = EventDispatcher._addObserver + EventDispatcher._addObserver = _addObserver + + def streamStarted(self, root=None): + if root: + self.xmlstream.sid = '%x' % random.randint(1, sys.maxint) + + self.xmlstream.sendHeader() + self.xmlstream.addOnetimeObserver( + "/iq/query[@xmlns='jabber:iq:auth']", self.initialIq) + + def initialIq(self, iq): + result = IQ(self.xmlstream, "result") + result["id"] = iq["id"] + query = result.addElement('query') + query["xmlns"] = "jabber:iq:auth" + query.addElement('username', content='test') + query.addElement('password') + query.addElement('digest') + query.addElement('resource') + self.xmlstream.addOnetimeObserver('/iq/query/username', self.secondIq) + self.xmlstream.send(result) + + def secondIq(self, iq): + username = xpath.queryForNodes('/iq/query/username', iq) + assert map(str, username) == [self.username] + + digest = xpath.queryForNodes('/iq/query/digest', iq) + expect = hashlib.sha1(self.xmlstream.sid + self.password).hexdigest() + assert map(str, digest) == [expect] + + resource = xpath.queryForNodes('/iq/query/resource', iq) + assertLength(1, resource) + if self.resource is not None: + assertEquals(self.resource, str(resource[0])) + + result = IQ(self.xmlstream, "result") + result["id"] = iq["id"] + self.xmlstream.send(result) + self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) + + +class XmppAuthenticator(xmlstream.Authenticator): + def __init__(self, username, password, resource=None): + xmlstream.Authenticator.__init__(self) + self.username = username + self.password = password + self.resource = resource + self.authenticated = False + + def streamStarted(self, root=None): + if root: + self.xmlstream.sid = root.getAttribute('id') + + self.xmlstream.sendHeader() + + if self.authenticated: + # Initiator authenticated itself, and has started a new stream. + + features = domish.Element((xmlstream.NS_STREAMS, 'features')) + bind = features.addElement((NS_XMPP_BIND, 'bind')) + self.xmlstream.send(features) + + self.xmlstream.addOnetimeObserver( + "/iq/bind[@xmlns='%s']" % NS_XMPP_BIND, self.bindIq) + else: + features = domish.Element((xmlstream.NS_STREAMS, 'features')) + mechanisms = features.addElement((NS_XMPP_SASL, 'mechanisms')) + mechanism = mechanisms.addElement('mechanism', content='PLAIN') + self.xmlstream.send(features) + + self.xmlstream.addOnetimeObserver("/auth", self.auth) + + def auth(self, auth): + assert (base64.b64decode(str(auth)) == + '\x00%s\x00%s' % (self.username, self.password)) + + success = domish.Element((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_XMPP_BIND, 'bind')) + jid = bind.addElement('jid', content=('test@localhost/%s' % resource)) + self.xmlstream.send(result) + + self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) + +def make_stream_event(type, stanza): + event = servicetest.Event(type, stanza=stanza) + event.to = stanza.getAttribute("to") + return event + +def make_iq_event(iq): + event = make_stream_event('stream-iq', iq) + event.iq_type = iq.getAttribute("type") + event.iq_id = iq.getAttribute("id") + query = iq.firstChildElement() + + if query: + event.query = query + event.query_ns = query.uri + event.query_name = query.name + + if query.getAttribute("node"): + event.query_node = query.getAttribute("node") + else: + event.query = None + + return event + +def make_presence_event(stanza): + event = make_stream_event('stream-presence', stanza) + event.presence_type = stanza.getAttribute('type') + return event + +def make_message_event(stanza): + event = make_stream_event('stream-message', stanza) + event.message_type = stanza.getAttribute('type') + return event + +class BaseXmlStream(xmlstream.XmlStream): + initiating = False + namespace = 'jabber:client' + + def __init__(self, event_func, authenticator): + xmlstream.XmlStream.__init__(self, authenticator) + self.event_func = event_func + self.addObserver('//iq', lambda x: event_func( + make_iq_event(x))) + self.addObserver('//message', lambda x: event_func( + make_message_event(x))) + self.addObserver('//presence', lambda x: event_func( + make_presence_event(x))) + self.addObserver('//event/stream/authd', self._cb_authd) + + def _cb_authd(self, _): + # called when stream is authenticated + self.addObserver( + "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']", + self._cb_disco_iq) + self.event_func(servicetest.Event('stream-authenticated')) + + def _cb_disco_iq(self, iq): + if iq.getAttribute('to') == 'localhost': + # add 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 + +class JabberXmlStream(BaseXmlStream): + version = (0, 9) + +class XmppXmlStream(BaseXmlStream): + version = (1, 0) + +class GoogleXmlStream(BaseXmlStream): + version = (1, 0) + + def _cb_disco_iq(self, iq): + if iq.getAttribute('to') == 'localhost': + nodes = xpath.queryForNodes( + "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']", + iq) + query = nodes[0] + feature = query.addElement('feature') + feature['var'] = ns.GOOGLE_ROSTER + feature = query.addElement('feature') + feature['var'] = ns.GOOGLE_JINGLE_INFO + + iq['type'] = 'result' + iq['from'] = 'localhost' + self.send(iq) + +def make_connection(bus, event_func, params=None): + # Gabble accepts a resource in 'account', but the value of 'resource' + # overrides it if there is one. + account = 'test@localhost/%s' % re.sub(r'.*tests/twisted/', '', sys.argv[0]) + default_params = { + 'account': account, + 'password': 'pass', + 'resource': 'Resource', + 'server': 'localhost', + 'port': dbus.UInt32(4242), + 'fallback-socks5-proxies': dbus.Array([], signature='s'), + } + + if params: + default_params.update(params) + + return servicetest.make_connection(bus, event_func, 'gabble', 'jabber', + default_params) + +def make_stream(event_func, authenticator=None, protocol=None, port=4242, resource=None): + # set up Jabber server + + if authenticator is None: + authenticator = XmppAuthenticator('test', 'pass', resource=resource) + + if protocol is None: + protocol = XmppXmlStream + + stream = protocol(event_func, authenticator) + factory = twisted.internet.protocol.Factory() + factory.protocol = lambda *args: stream + port = reactor.listenTCP(port, factory) + return (stream, port) + +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): + # 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() + + queue = servicetest.IteratingEventQueue(timeout) + queue.verbose = ( + os.environ.get('CHECK_TWISTED_VERBOSE', '') != '' + or '-v' in sys.argv) + + bus = dbus.SessionBus() + conn = make_connection(bus, queue.append, params) + resource = params.get('resource') if params is not None else None + (stream, port) = make_stream(queue.append, protocol=protocol, + authenticator=authenticator, resource=resource) + + error = None + + try: + fun(queue, bus, conn, stream) + except Exception, e: + traceback.print_exc() + error = e + + if colourer: + sys.stdout = colourer.fh + + d = port.stopListening() + + 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))) + + # Does the Connection object still exist? + if not bus.name_has_owner(conn.object.bus_name): + # Connection has already been disconnected and destroyed + return + + try: + if conn.GetStatus() == cs.CONN_STATUS_CONNECTED: + # Connection is connected, properly disconnect it + disconnect_conn(queue, conn, stream) + else: + # Connection is not connected, call Disconnect() to destroy it + conn.Disconnect() + except dbus.DBusException, e: + pass + +def exec_test(fun, params=None, protocol=None, timeout=None, + authenticator=None): + reactor.callWhenRunning( + exec_test_deferred, fun, params, protocol, timeout, authenticator) + reactor.run() + +# Useful routines for server-side vCard handling +current_vcard = domish.Element(('vcard-temp', 'vCard')) + +def expect_and_handle_get_vcard(q, stream): + get_vcard_event = q.expect('stream-iq', query_ns=ns.VCARD_TEMP, + query_name='vCard', iq_type='get') + + iq = get_vcard_event.stanza + vcard = iq.firstChildElement() + assert vcard.name == 'vCard', vcard.toXml() + + # Send back current vCard + result = make_result_iq(stream, iq) + result.addChild(current_vcard) + stream.send(result) + +def expect_and_handle_set_vcard(q, stream, check=None): + set_vcard_event = q.expect('stream-iq', query_ns=ns.VCARD_TEMP, + query_name='vCard', iq_type='set') + iq = set_vcard_event.stanza + vcard = iq.firstChildElement() + assert vcard.name == 'vCard', vcard.toXml() + + if check is not None: + check(vcard) + + # Update current vCard + current_vcard = vcard + + stream.send(make_result_iq(stream, iq)) + +def _elem_add(elem, *children): + for child in children: + if isinstance(child, domish.Element): + elem.addChild(child) + elif isinstance(child, unicode): + elem.addContent(child) + else: + raise ValueError( + 'invalid child object %r (must be element or unicode)', child) + +def elem(a, b=None, **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>" + """ + + 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)) + + for k, v in kw.iteritems(): + if k == 'from_': + elem['from'] = v + else: + elem[k] = v + + return elem + +def elem_iq(server, type, **kw): + class _iq(IQ): + def __call__(self, *children): + _elem_add(self, *children) + return self + + iq = _iq(server, type) + + for k, v in kw.iteritems(): + if k == 'from_': + iq['from'] = v + else: + iq[k] = v + + return iq + +def make_presence(_from, to='test@localhost', type=None, show=None, + status=None, caps=None, photo=None): + presence = domish.Element((None, 'presence')) + presence['from'] = _from + presence['to'] = to + + if type is not None: + presence['type'] = type + + if show is not None: + presence.addElement('show', content=show) + + if status is not None: + presence.addElement('status', content=status) + + if caps is not None: + cel = presence.addElement(('http://jabber.org/protocol/caps', 'c')) + for key,value in caps.items(): + cel[key] = value + + # <x xmlns="vcard-temp:x:update"><photo>4a1...</photo></x> + if photo is not None: + x = presence.addElement((ns.VCARD_TEMP_UPDATE, 'x')) + x.addElement('photo').addContent(photo) + + return presence + +def expect_list_channel(q, bus, conn, name, contacts, lp_contacts=[], + rp_contacts=[]): + return expect_contact_list_channel(q, bus, conn, cs.HT_LIST, name, + contacts, lp_contacts=lp_contacts, rp_contacts=rp_contacts) + +def expect_group_channel(q, bus, conn, name, contacts, lp_contacts=[], + rp_contacts=[]): + return expect_contact_list_channel(q, bus, conn, cs.HT_GROUP, name, + contacts, lp_contacts=lp_contacts, rp_contacts=rp_contacts) + +def expect_contact_list_channel(q, bus, conn, ht, name, contacts, + lp_contacts=[], rp_contacts=[]): + """ + Expects NewChannel and NewChannels signals for the + contact list with handle type 'ht' and ID 'name', and checks that its + members, lp members and rp members are exactly 'contacts', 'lp_contacts' + and 'rp_contacts'. + Returns a proxy for the channel. + """ + + old_signal, new_signal = q.expect_many( + EventPattern('dbus-signal', signal='NewChannel'), + EventPattern('dbus-signal', signal='NewChannels'), + ) + + path, type, handle_type, handle, suppress_handler = old_signal.args + + assertEquals(cs.CHANNEL_TYPE_CONTACT_LIST, type) + assertEquals(name, conn.InspectHandles(handle_type, [handle])[0]) + + chan = wrap_channel(bus.get_object(conn.bus_name, path), + cs.CHANNEL_TYPE_CONTACT_LIST) + members = chan.Group.GetMembers() + + assertEquals(sorted(contacts), + sorted(conn.InspectHandles(cs.HT_CONTACT, members))) + + lp_handles = conn.RequestHandles(cs.HT_CONTACT, lp_contacts) + rp_handles = conn.RequestHandles(cs.HT_CONTACT, rp_contacts) + + # NB. comma: we're unpacking args. Thython! + info, = new_signal.args + assertLength(1, info) # one channel + path_, emitted_props = info[0] + + assertEquals(path_, path) + + assertEquals(cs.CHANNEL_TYPE_CONTACT_LIST, emitted_props[cs.CHANNEL_TYPE]) + assertEquals(ht, emitted_props[cs.TARGET_HANDLE_TYPE]) + assertEquals(handle, emitted_props[cs.TARGET_HANDLE]) + + channel_props = chan.Properties.GetAll(cs.CHANNEL) + assertEquals(handle, channel_props.get('TargetHandle')) + assertEquals(ht, channel_props.get('TargetHandleType')) + assertEquals(cs.CHANNEL_TYPE_CONTACT_LIST, channel_props.get('ChannelType')) + assertContains(cs.CHANNEL_IFACE_GROUP, channel_props.get('Interfaces')) + assertEquals(name, channel_props['TargetID']) + assertEquals(False, channel_props['Requested']) + assertEquals('', channel_props['InitiatorID']) + assertEquals(0, channel_props['InitiatorHandle']) + + group_props = chan.Properties.GetAll(cs.CHANNEL_IFACE_GROUP) + assertContains('HandleOwners', group_props) + assertContains('Members', group_props) + assertEquals(members, group_props['Members']) + assertContains('LocalPendingMembers', group_props) + actual_lp_handles = [x[0] for x in group_props['LocalPendingMembers']] + assertEquals(sorted(lp_handles), sorted(actual_lp_handles)) + assertContains('RemotePendingMembers', group_props) + assertEquals(sorted(rp_handles), sorted(group_props['RemotePendingMembers'])) + assertContains('GroupFlags', group_props) + + return chan diff --git a/tests/twisted/gateways.py b/tests/twisted/gateways.py new file mode 100644 index 0000000..bce152e --- /dev/null +++ b/tests/twisted/gateways.py @@ -0,0 +1,94 @@ +""" +Test the gateways plugin +""" + +import dbus +from twisted.words.xish import domish, xpath + +from servicetest import ( + sync_dbus, call_async, EventPattern, assertEquals, assertContains, + ) +from gabbletest import exec_test, send_error_reply, acknowledge_iq, sync_stream +import constants as cs +import ns +from config import PLUGINS_ENABLED + +PLUGIN_IFACE = "org.freedesktop.Telepathy.Gabble.Plugin.Gateways" + +if not PLUGINS_ENABLED: + print "NOTE: built without --enable-plugins, not testing plugins" + raise SystemExit(77) + +def test_success(q, gateways_iface, stream): + call_async(q, gateways_iface, 'Register', + 'talkd.example.com', '1970', 's3kr1t') + e = q.expect('stream-iq', iq_type='set', query_name='query', + query_ns=ns.REGISTER, to='talkd.example.com') + assertEquals('1970', xpath.queryForString('/query/username', e.query)) + assertEquals('s3kr1t', xpath.queryForString('/query/password', e.query)) + acknowledge_iq(stream, e.stanza) + q.expect('dbus-return', method='Register') + +def test_conflict(q, gateways_iface, stream): + call_async(q, gateways_iface, 'Register', + 'sip.example.com', '8675309', 'jenny') + e = q.expect('stream-iq', iq_type='set', query_name='query', + query_ns=ns.REGISTER, to='sip.example.com') + assertEquals('8675309', xpath.queryForString('/query/username', e.query)) + assertEquals('jenny', xpath.queryForString('/query/password', e.query)) + error = domish.Element((None, 'error')) + error['type'] = 'cancel' + error['code'] = '409' + error.addElement((ns.STANZA, 'conflict')) + send_error_reply(stream, e.stanza, error) + q.expect('dbus-error', method='Register', name=cs.REGISTRATION_EXISTS) + +def test_not_acceptable(q, gateways_iface, stream): + call_async(q, gateways_iface, 'Register', + 'fully-captcha-enabled.example.com', 'lalala', 'stoats') + e = q.expect('stream-iq', iq_type='set', query_name='query', + query_ns=ns.REGISTER, to='fully-captcha-enabled.example.com') + assertEquals('lalala', xpath.queryForString('/query/username', e.query)) + assertEquals('stoats', xpath.queryForString('/query/password', e.query)) + error = domish.Element((None, 'error')) + error['type'] = 'modify' + error['code'] = '406' + error.addElement((ns.STANZA, 'not-acceptable')) + send_error_reply(stream, e.stanza, error) + q.expect('dbus-error', method='Register', name=cs.NOT_AVAILABLE) + +def test(q, bus, conn, stream): + # Request a sidecar thate we support before we're connected; it should just + # wait around until we're connected. + call_async(q, conn.Future, 'EnsureSidecar', PLUGIN_IFACE) + + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + # Now we're connected, the call we made earlier should return. + path, props = q.expect('dbus-return', method='EnsureSidecar').value + # This sidecar doesn't even implement get_immutable_properties; it + # should just get the empty dict filled in for it. + assertEquals({}, props) + + gateways_iface = dbus.Interface(bus.get_object(conn.bus_name, path), + PLUGIN_IFACE) + + test_success(q, gateways_iface, stream) + test_conflict(q, gateways_iface, stream) + test_not_acceptable(q, gateways_iface, stream) + + call_async(q, conn, 'Disconnect') + + q.expect_many( + EventPattern('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_DISCONNECTED, cs.CSR_REQUESTED]), + EventPattern('stream-closed'), + ) + + stream.sendFooter() + q.expect('dbus-return', method='Disconnect') + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/httptest.py b/tests/twisted/httptest.py new file mode 100644 index 0000000..ce9040c --- /dev/null +++ b/tests/twisted/httptest.py @@ -0,0 +1,31 @@ + +from twisted.web import http +from twisted.internet import reactor + +from servicetest import Event + +class Request(http.Request): + def process(self): + self.queue.append(Event('http-request', + method=self.method, path=self.path, request=self)) + +class HTTPChannel(http.HTTPChannel): + def requestFactory(self, *misc): + request = Request(*misc) + request.queue = self.queue + return request + +class HTTPFactory(http.HTTPFactory): + protocol = HTTPChannel + + def __init__(self, queue): + self.queue = queue + + def buildProtocol(self, addr): + protocol = http.HTTPFactory.buildProtocol(self, addr) + protocol.queue = self.queue + return protocol + +def listen_http(q, port=0): + return reactor.listenTCP(port, HTTPFactory(q)) + diff --git a/tests/twisted/main-debug.c b/tests/twisted/main-debug.c new file mode 100644 index 0000000..d7569c2 --- /dev/null +++ b/tests/twisted/main-debug.c @@ -0,0 +1,73 @@ +/* + * main.c - entry point for telepathy-gabble-debug used by tests + * Copyright (C) 2008 Collabora Ltd. + * + * 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 + */ + +#include <stdlib.h> + +#include "gabble.h" +#include "connection.h" +#include "vcard-manager.h" +#include "jingle-factory.h" +#include "jingle-session.h" + +#include "test-resolver.h" + +#include <dbus/dbus.h> + +int +main (int argc, + char **argv) +{ + int ret = 1; + GResolver *kludged; + + gabble_init (); + + /* needed for test-disco-no-reply.py */ + gabble_connection_set_disco_reply_timeout (3); + /* needed for test-avatar-async.py */ + gabble_vcard_manager_set_suspend_reply_timeout (3); + gabble_vcard_manager_set_default_request_timeout (3); + + /* hook up the fake DNS resolver that lets us divert A and SRV queries * + * into our local cache before asking the real DNS */ + kludged = g_object_new (TEST_TYPE_RESOLVER, NULL); + g_resolver_set_default (kludged); + g_object_unref (kludged); + + test_resolver_add_A (TEST_RESOLVER (kludged), + "resolves-to-5.4.3.2", "5.4.3.2"); + test_resolver_add_A (TEST_RESOLVER (kludged), + "resolves-to-1.2.3.4", "1.2.3.4"); + test_resolver_add_A (TEST_RESOLVER (kludged), + "localhost", "127.0.0.1"); + test_resolver_add_A (TEST_RESOLVER (kludged), + "stun.telepathy.im", "6.7.8.9"); + + gabble_jingle_factory_set_test_mode (); + + ret = gabble_main (argc, argv); + + /* Hack, remove the ref g_resolver has on this object, atm there is no way to + * unset a custom resolver */ + g_object_unref (kludged); + + dbus_shutdown (); + + return ret; +} diff --git a/tests/twisted/mucutil.py b/tests/twisted/mucutil.py new file mode 100644 index 0000000..3c30ea7 --- /dev/null +++ b/tests/twisted/mucutil.py @@ -0,0 +1,70 @@ +""" +Utility functions for tests that need to interact with MUCs. +""" + +import dbus + +from servicetest import call_async, wrap_channel, EventPattern +from gabbletest import make_muc_presence, request_muc_handle + +import constants as cs +import ns + +def join_muc(q, bus, conn, stream, muc, request=None, + also_capture=[], role='participant'): + """ + Joins 'muc', returning the muc's handle, a proxy object for the channel, + its path and its immutable properties just after the CreateChannel event + has fired. The room contains one other member. + """ + if request is None: + request = { + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_ROOM, + cs.TARGET_ID: muc, + } + + muc_handle = request_muc_handle(q, conn, stream, muc) + + requests = dbus.Interface(conn, cs.CONN_IFACE_REQUESTS) + call_async(q, requests, 'CreateChannel', + dbus.Dictionary(request, signature='sv')) + + q.expect('stream-presence', to='%s/test' % muc) + + # Send presence for other member of room. + stream.send(make_muc_presence('owner', 'moderator', muc, 'bob')) + + # Send presence for own membership of room. + stream.send(make_muc_presence('none', role, muc, 'test')) + + captured = q.expect_many( + EventPattern('dbus-return', method='CreateChannel'), + *also_capture) + path, props = captured[0].value + chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text', + ['Messages']) + + return (muc_handle, chan, path, props) + tuple(captured[1:]) + +def join_muc_and_check(q, bus, conn, stream, muc, request=None): + """ + Like join_muc(), but also checks the NewChannels and NewChannel signals and + the Members property, and returns both members' handles. + """ + muc_handle, chan, path, props = \ + join_muc(q, bus, conn, stream, muc, request=request) + + q.expect('dbus-signal', signal='NewChannels', args=[[(path, props)]]) + q.expect('dbus-signal', signal='NewChannel', + args=[path, cs.CHANNEL_TYPE_TEXT, cs.HT_ROOM, muc_handle, True]) + + test_handle, bob_handle = conn.RequestHandles(cs.HT_CONTACT, + ['%s/test' % muc, '%s/bob' % muc]) + + members = chan.Get(cs.CHANNEL_IFACE_GROUP, 'Members', + dbus_interface=cs.PROPERTIES_IFACE) + assert set(members) == set([test_handle, bob_handle]), \ + (members, (test_handle, bob_handle)) + + return (muc_handle, chan, test_handle, bob_handle) diff --git a/tests/twisted/ns.py b/tests/twisted/ns.py new file mode 100644 index 0000000..2191259 --- /dev/null +++ b/tests/twisted/ns.py @@ -0,0 +1,61 @@ +AMP = "http://jabber.org/protocol/amp" +BYTESTREAMS = 'http://jabber.org/protocol/bytestreams' +CHAT_STATES = 'http://jabber.org/protocol/chatstates' +CAPS = "http://jabber.org/protocol/caps" +DISCO_INFO = "http://jabber.org/protocol/disco#info" +DISCO_ITEMS = "http://jabber.org/protocol/disco#items" +FEATURE_NEG = 'http://jabber.org/protocol/feature-neg' +FILE_TRANSFER = 'http://jabber.org/protocol/si/profile/file-transfer' +GEOLOC = 'http://jabber.org/protocol/geoloc' +GOOGLE_FEAT_SESSION = 'http://www.google.com/xmpp/protocol/session' +GOOGLE_FEAT_VOICE = 'http://www.google.com/xmpp/protocol/voice/v1' +GOOGLE_FEAT_VIDEO = 'http://www.google.com/xmpp/protocol/video/v1' +GOOGLE_JINGLE_INFO = 'google:jingleinfo' +GOOGLE_P2P = "http://www.google.com/transport/p2p" +GOOGLE_ROSTER = 'google:roster' +GOOGLE_SESSION = "http://www.google.com/session" +GOOGLE_SESSION_PHONE = "http://www.google.com/session/phone" +GOOGLE_SESSION_VIDEO = "http://www.google.com/session/video" +IBB = 'http://jabber.org/protocol/ibb' +JINGLE_015 = "http://jabber.org/protocol/jingle" +JINGLE_015_AUDIO = "http://jabber.org/protocol/jingle/description/audio" +JINGLE_015_VIDEO = "http://jabber.org/protocol/jingle/description/video" +JINGLE = "urn:xmpp:jingle:1" +JINGLE_RTP = "urn:xmpp:jingle:apps:rtp:1" +JINGLE_RTP_AUDIO = "urn:xmpp:jingle:apps:rtp:audio" +JINGLE_RTP_VIDEO = "urn:xmpp:jingle:apps:rtp:video" +JINGLE_RTP_ERRORS = "urn:xmpp:jingle:apps:rtp:errors:1" +JINGLE_RTP_INFO_1 = "urn:xmpp:jingle:apps:rtp:info:1" +JINGLE_TRANSPORT_ICEUDP = "urn:xmpp:jingle:transports:ice-udp:1" +JINGLE_TRANSPORT_RAWUDP = "urn:xmpp:jingle:transports:raw-udp:1" +MUC = 'http://jabber.org/protocol/muc' +MUC_BYTESTREAM = 'http://telepathy.freedesktop.org/xmpp/protocol/muc-bytestream' +MUC_OWNER = '%s#owner' % MUC +MUC_USER = '%s#user' % MUC +NICK = "http://jabber.org/protocol/nick" +OLPC_ACTIVITIES = "http://laptop.org/xmpp/activities" +OLPC_ACTIVITIES_NOTIFY = "%s+notify" % OLPC_ACTIVITIES +OLPC_ACTIVITY = "http://laptop.org/xmpp/activity" +OLPC_ACTIVITY_PROPS = "http://laptop.org/xmpp/activity-properties" +OLPC_ACTIVITY_PROPS_NOTIFY = "%s+notify" % OLPC_ACTIVITY_PROPS +OLPC_BUDDY = "http://laptop.org/xmpp/buddy" +OLPC_BUDDY_PROPS = "http://laptop.org/xmpp/buddy-properties" +OLPC_BUDDY_PROPS_NOTIFY = "%s+notify" % OLPC_BUDDY_PROPS +OLPC_CURRENT_ACTIVITY = "http://laptop.org/xmpp/current-activity" +OLPC_CURRENT_ACTIVITY_NOTIFY = "%s+notify" % OLPC_CURRENT_ACTIVITY +PUBSUB = "http://jabber.org/protocol/pubsub" +PUBSUB_EVENT = "%s#event" % PUBSUB +REGISTER = "jabber:iq:register" +ROSTER = "jabber:iq:roster" +SEARCH = 'jabber:iq:search' +SI = 'http://jabber.org/protocol/si' +SI_MULTIPLE = 'http://telepathy.freedesktop.org/xmpp/si-multiple' +STANZA = "urn:ietf:params:xml:ns:xmpp-stanzas" +STREAMS = "urn:ietf:params:xml:ns:xmpp-streams" +TEMPPRES = "urn:xmpp:temppres:0" +TUBES = 'http://telepathy.freedesktop.org/xmpp/tubes' +VCARD_TEMP = 'vcard-temp' +VCARD_TEMP_UPDATE = 'vcard-temp:x:update' +X_DATA = 'jabber:x:data' +XML = 'http://www.w3.org/XML/1998/namespace' +X_OOB = 'jabber:x:oob' diff --git a/tests/twisted/pubsub.py b/tests/twisted/pubsub.py new file mode 100644 index 0000000..70de708 --- /dev/null +++ b/tests/twisted/pubsub.py @@ -0,0 +1,88 @@ +"""Send malformed pubsub notifications to be sure that Gabble isn't confused about those""" +from gabbletest import exec_test, elem, sync_stream + +import constants as cs +import ns + +def test(q, bus, conn, stream): + conn.Connect() + + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + # event node without NS + message = elem('message', from_='bob@foo.com')( + elem('event')( + elem('items', node=ns.GEOLOC)( + elem('item', id='12345')( + elem(ns.GEOLOC, 'geoloc')( + elem ('country') (u'France')))))) + stream.send(message) + + # event node with a wrong NS + message = elem('message', from_='bob@foo.com')( + elem('badger', 'event')( + elem('items', node=ns.GEOLOC)( + elem('item', id='12345')( + elem(ns.GEOLOC, 'geoloc')( + elem ('country') (u'France')))))) + stream.send(message) + + # event node without 'from' + message = elem('message')( + elem((ns.PUBSUB_EVENT), 'event')( + elem('items', node=ns.GEOLOC)( + elem('item', id='12345')( + elem(ns.GEOLOC, 'geoloc')( + elem ('country') (u'France')))))) + stream.send(message) + + # event node with an invalid 'from' + message = elem('message', from_='aaaa')( + elem((ns.PUBSUB_EVENT), 'event')( + elem('items', node=ns.GEOLOC)( + elem('item', id='12345')( + elem(ns.GEOLOC, 'geoloc')( + elem ('country') (u'France')))))) + stream.send(message) + + # no items node + message = elem('message', from_='bob@foo.com')( + elem((ns.PUBSUB_EVENT), 'event')()) + stream.send(message) + + # no item node + message = elem('message', from_='bob@foo.com')( + elem((ns.PUBSUB_EVENT), 'event')( + elem('items', node=ns.GEOLOC)())) + stream.send(message) + + # item node doesn't have any child + message = elem('message', from_='bob@foo.com')( + elem((ns.PUBSUB_EVENT), 'event')( + elem('items', node=ns.GEOLOC)( + elem('item', id='12345')()))) + stream.send(message) + + # the child of the item node doesn't have a NS + message = elem('message', from_='bob@foo.com')( + elem((ns.PUBSUB_EVENT), 'event')( + elem('items', node=ns.GEOLOC)( + elem('item', id='12345')( + elem('geoloc')( + elem ('country') (u'France')))))) + stream.send(message) + + # valid but unknown pubsub notification + message = elem('message', from_='bob@foo.com')( + elem((ns.PUBSUB_EVENT), 'event')( + elem('items', node='http://www.badger.com')( + elem('item', id='12345')( + elem('http://www.badger.com', 'badger')( + elem ('mushroom') (u'snake')))))) + stream.send(message) + + sync_stream(q, stream) + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/servicetest.py b/tests/twisted/servicetest.py new file mode 100644 index 0000000..0634678 --- /dev/null +++ b/tests/twisted/servicetest.py @@ -0,0 +1,510 @@ + +""" +Infrastructure code for testing connection managers. +""" + +from twisted.internet import glib2reactor +from twisted.internet.protocol import Protocol, Factory, ClientFactory +glib2reactor.install() +import sys + +import pprint +import unittest + +import dbus.glib + +from twisted.internet import reactor + +import constants as cs + +tp_name_prefix = 'org.freedesktop.Telepathy' +tp_path_prefix = '/org/freedesktop/Telepathy' + +class Event: + def __init__(self, type, **kw): + self.__dict__.update(kw) + self.type = type + +def format_event(event): + ret = ['- type %s' % event.type] + + for key in 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 = lambda x: True + if 'predicate' in properties: + self.predicate = properties['predicate'] + del properties['predicate'] + self.properties = properties + + def __repr__(self): + properties = dict(self.properties) + + if self.predicate: + 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(event): + return True + + return False + + +class TimeoutError(Exception): + pass + +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() + + if timeout is None: + self.timeout = 5 + else: + self.timeout = timeout + + def log(self, s): + if self.verbose: + print s + + def log_event(self, event): + if self.verbose: + 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): + print "forbidden event occurred:" + for x in format_event(event): + print x + assert False + + def expect(self, type, **kw): + pattern = EventPattern(type, **kw) + + while True: + event = self.wait() + self.log_event(event) + self._check_forbidden(event) + + if pattern.match(event): + self.log('handled') + self.log('') + return event + + self.log('not handled') + self.log('') + + def expect_many(self, *patterns): + ret = [None] * len(patterns) + + while None in ret: + try: + event = self.wait() + except TimeoutError: + self.log('timeout') + self.log('still expecting:') + for i, pattern in enumerate(patterns): + if ret[i] is None: + self.log(' - %r' % pattern) + raise + self.log_event(event) + self._check_forbidden(event) + + for i, pattern in enumerate(patterns): + if ret[i] is None and pattern.match(event): + self.log('handled') + 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() + self.log_event(event) + + if pattern.match(event): + self.log('handled') + self.log('') + return event + + self.log('not handled') + raise RuntimeError('expected %r, got %r' % (pattern, event)) + +class IteratingEventQueue(BaseEventQueue): + """Event queue that works by iterating the Twisted reactor.""" + + def __init__(self, timeout=None): + BaseEventQueue.__init__(self, timeout) + self.events = [] + + def wait(self): + stop = [False] + + def later(): + stop[0] = True + + delayed_call = reactor.callLater(self.timeout, later) + + while (not self.events) and (not stop[0]): + reactor.iterate(0.1) + + if self.events: + delayed_call.cancel() + return self.events.pop(0) + else: + raise TimeoutError + + def append(self, event): + self.events.append(event) + + # compatibility + handle_event = append + +class TestEventQueue(BaseEventQueue): + def __init__(self, events): + BaseEventQueue.__init__(self) + self.events = events + + def wait(self): + if self.events: + return self.events.pop(0) + else: + raise TimeoutError + +class EventQueueTest(unittest.TestCase): + def test_expect(self): + queue = TestEventQueue([Event('foo'), Event('bar')]) + assert queue.expect('foo').type == 'foo' + assert queue.expect('bar').type == 'bar' + + def test_expect_many(self): + queue = TestEventQueue([Event('foo'), Event('bar')]) + bar, foo = queue.expect_many( + EventPattern('bar'), + EventPattern('foo')) + assert bar.type == 'bar' + assert foo.type == 'foo' + + def test_expect_many2(self): + # Test that events are only matched against patterns that haven't yet + # been matched. This tests a regression. + queue = TestEventQueue([Event('foo', x=1), Event('foo', x=2)]) + foo1, foo2 = queue.expect_many( + EventPattern('foo'), + EventPattern('foo')) + assert foo1.type == 'foo' and foo1.x == 1 + assert foo2.type == 'foo' and foo2.x == 2 + + def test_timeout(self): + queue = TestEventQueue([]) + self.assertRaises(TimeoutError, queue.expect, 'foo') + + def test_demand(self): + queue = TestEventQueue([Event('foo'), Event('bar')]) + foo = queue.demand('foo') + assert foo.type == 'foo' + + def test_demand_fail(self): + queue = TestEventQueue([Event('foo'), Event('bar')]) + self.assertRaises(RuntimeError, queue.demand, '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.handle_event(Event('dbus-return', method=method, + value=unwrap(ret))) + + def error_func(err): + test.handle_event(Event('dbus-error', method=method, error=err, + name=err.get_dbus_name(), message=str(err))) + + method_proxy = getattr(proxy, method) + kw.update({'reply_handler': reply_func, 'error_handler': error_func}) + method_proxy(*args, **kw) + +def sync_dbus(bus, q, conn): + # Dummy D-Bus method call + # This won't do the right thing unless the proxy has a unique name. + assert conn.object.bus_name.startswith(':') + root_object = bus.get_object(conn.object.bus_name, '/') + call_async( + q, dbus.Interface(root_object, 'org.freedesktop.DBus.Peer'), 'Ping') + q.expect('dbus-return', method='Ping') + +class ProxyWrapper: + def __init__(self, object, default, others): + self.object = object + self.default_interface = dbus.Interface(object, default) + self.Properties = dbus.Interface(object, dbus.PROPERTIES_IFACE) + self.TpProperties = \ + dbus.Interface(object, tp_name_prefix + '.Properties') + self.interfaces = dict([ + (name, dbus.Interface(object, iface)) + for name, iface in others.iteritems()]) + + 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), + ('Location', cs.CONN_IFACE_LOCATION), + ('Future', tp_name_prefix + '.Connection.FUTURE'), + ])) + +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) + cm_iface = dbus.Interface(cm, tp_name_prefix + '.ConnectionManager') + + connection_name, connection_path = cm_iface.RequestConnection( + proto, params) + conn = wrap_connection(bus.get_object(connection_name, connection_path)) + + bus.add_signal_receiver( + lambda *args, **kw: + event_func( + Event('dbus-signal', + path=unwrap(kw['path']), + signal=kw['member'], args=map(unwrap, args), + interface=kw['interface'])), + None, # signal name + None, # interface + None, + path_keyword='path', + member_keyword='member', + interface_keyword='interface', + byte_arrays=True + ) + + 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.handle_event(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.handle_event(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.handle_event(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.handle_event(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 assertNotEquals(expected, value): + if expected == value: + raise AssertionError( + "expected something other than:\n%s" % pretty(value)) + +def assertContains(element, value): + if element not in value: + raise AssertionError( + "expected:\n%s\nin:\n%s" % (pretty(element), pretty(value))) + +def assertDoesNotContain(element, value): + if element in value: + raise AssertionError( + "expected:\n%s\nnot in:\n%s" % (pretty(element), pretty(value))) + +def assertLength(length, value): + if len(value) != length: + raise AssertionError("expected: length %d, got length %d:\n%s" % ( + length, len(value), pretty(value))) + +def assertFlagsSet(flags, value): + masked = value & flags + if masked != flags: + raise AssertionError( + "expected flags %u, of which only %u are set in %u" % ( + flags, masked, value)) + +def assertFlagsUnset(flags, value): + masked = value & flags + if masked != 0: + raise AssertionError( + "expected none of flags %u, but %u are set in %u" % ( + flags, masked, value)) + +def install_colourer(): + def red(s): + return '\x1b[31m%s\x1b[0m' % s + + def green(s): + return '\x1b[32m%s\x1b[0m' % s + + patterns = { + 'handled': green, + 'not handled': red, + } + + class Colourer: + def __init__(self, fh, patterns): + self.fh = fh + self.patterns = patterns + + def write(self, s): + f = self.patterns.get(s, lambda x: x) + self.fh.write(f(s)) + + sys.stdout = Colourer(sys.stdout, patterns) + return sys.stdout + +if __name__ == '__main__': + unittest.main() + diff --git a/tests/twisted/test-debug.py b/tests/twisted/test-debug.py new file mode 100644 index 0000000..2614eb3 --- /dev/null +++ b/tests/twisted/test-debug.py @@ -0,0 +1,55 @@ + +""" +Test the debug message interface. +""" + +import dbus + +from gabbletest import exec_test +import constants as cs +from config import DEBUGGING + +if not DEBUGGING: + print " -- Not testing debugger, built with --disable-debug" + raise SystemExit(77) + +path = '/org/freedesktop/Telepathy/debug' +iface = 'org.freedesktop.Telepathy.Debug' + +def test(q, bus, conn, stream): + messages = [] + + def new_message(timestamp, domain, level, string): + messages.append((timestamp, domain, level, string)) + + debug = bus.get_object(conn.bus_name, path) + debug_iface = dbus.Interface(debug, iface) + debug_iface.connect_to_signal('NewDebugMessage', new_message) + props_iface = dbus.Interface(debug, cs.PROPERTIES_IFACE) + + assert len(debug_iface.GetMessages()) > 0 + + # Turn signalling on and generate some messages. + + assert len(messages) == 0 + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + assert props_iface.Get(iface, 'Enabled') == False + props_iface.Set(iface, 'Enabled', True) + + conn.RequestChannel( + cs.CHANNEL_TYPE_TEXT, cs.HT_CONTACT, conn.GetSelfHandle(), True) + q.expect('dbus-signal', signal='NewChannel') + assert len(messages) > 0 + + # Turn signalling off and check we have no new messages. + + props_iface.Set(iface, 'Enabled', False) + snapshot = list(messages) + + assert snapshot == messages + +if __name__ == '__main__': + exec_test(test) + diff --git a/tests/twisted/text/destroy.py b/tests/twisted/text/destroy.py new file mode 100644 index 0000000..8fcb918 --- /dev/null +++ b/tests/twisted/text/destroy.py @@ -0,0 +1,125 @@ +""" +Test text channel not being recreated because although there were still +pending messages, we destroyed it with extreme prejudice. +""" + +import dbus + +from twisted.words.xish import domish + +from gabbletest import exec_test +from servicetest import call_async, EventPattern +import constants as cs + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + self_handle = conn.GetSelfHandle() + + jid = 'foo@bar.com' + call_async(q, conn, 'RequestHandles', 1, [jid]) + + event = q.expect('dbus-return', method='RequestHandles') + foo_handle = event.value[0][0] + + call_async(q, conn, 'RequestChannel', + cs.CHANNEL_TYPE_TEXT, cs.HT_CONTACT, foo_handle, True) + + ret, old_sig, new_sig = q.expect_many( + EventPattern('dbus-return', method='RequestChannel'), + EventPattern('dbus-signal', signal='NewChannel'), + EventPattern('dbus-signal', signal='NewChannels'), + ) + + text_chan = bus.get_object(conn.bus_name, ret.value[0]) + chan_iface = dbus.Interface(text_chan, cs.CHANNEL) + text_iface = dbus.Interface(text_chan, cs.CHANNEL_TYPE_TEXT) + destroyable_iface = dbus.Interface(text_chan, cs.CHANNEL_IFACE_DESTROYABLE) + + assert old_sig.args[0] == ret.value[0] + assert old_sig.args[1] == cs.CHANNEL_TYPE_TEXT + assert old_sig.args[2] == cs.HT_CONTACT + assert old_sig.args[3] == foo_handle + assert old_sig.args[4] == True # suppress handler + + assert len(new_sig.args) == 1 + assert len(new_sig.args[0]) == 1 # one channel + assert len(new_sig.args[0][0]) == 2 # two struct members + assert new_sig.args[0][0][0] == ret.value[0] + emitted_props = new_sig.args[0][0][1] + assert emitted_props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_TEXT + assert emitted_props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT + assert emitted_props[cs.TARGET_HANDLE] == foo_handle + assert emitted_props[cs.TARGET_ID] == jid + assert emitted_props[cs.REQUESTED] == True + assert emitted_props[cs.INITIATOR_HANDLE] == self_handle + assert emitted_props[cs.INITIATOR_ID] == 'test@localhost' + + channel_props = text_chan.GetAll( + cs.CHANNEL, dbus_interface=dbus.PROPERTIES_IFACE) + assert channel_props['TargetID'] == jid, (channel_props['TargetID'], jid) + assert channel_props['Requested'] == True + assert channel_props['InitiatorHandle'] == self_handle,\ + (channel_props['InitiatorHandle'], self_handle) + assert channel_props['InitiatorID'] == 'test@localhost',\ + channel_props['InitiatorID'] + + text_iface.Send(0, 'hey') + + event = q.expect('stream-message') + + elem = event.stanza + assert elem.name == 'message' + assert elem['type'] == 'chat' + body = list(event.stanza.elements())[0] + assert body.name == 'body' + assert body.children[0] == u'hey' + + # <message type="chat"><body>hello</body</message> + m = domish.Element((None, 'message')) + m['from'] = 'foo@bar.com/Pidgin' + m['type'] = 'chat' + m.addElement('body', content='hello') + stream.send(m) + + event = q.expect('dbus-signal', signal='Received') + + hello_message_id = event.args[0] + hello_message_time = event.args[1] + assert event.args[2] == foo_handle + # message type: normal + assert event.args[3] == 0 + # flags: none + assert event.args[4] == 0 + # body + assert event.args[5] == 'hello' + + messages = text_chan.ListPendingMessages(False, + dbus_interface=cs.CHANNEL_TYPE_TEXT) + assert messages == \ + [(hello_message_id, hello_message_time, foo_handle, + 0, 0, 'hello')], messages + + # destroy the channel without acking the message; it does not come back + + call_async(q, destroyable_iface, 'Destroy') + + event = q.expect('dbus-signal', signal='Closed') + assert event.path == text_chan.object_path,\ + (event.path, text_chan.object_path) + + event = q.expect('dbus-return', method='Destroy') + + # assert that it stays dead + + try: + chan_iface.GetChannelType() + except dbus.DBusException: + pass + else: + raise AssertionError("Why won't it die?") + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/text/ensure.py b/tests/twisted/text/ensure.py new file mode 100644 index 0000000..5fdf4bf --- /dev/null +++ b/tests/twisted/text/ensure.py @@ -0,0 +1,171 @@ +""" +Test text channel initiated by me, using Requests.EnsureChannel +""" + +import dbus + +from gabbletest import exec_test +from servicetest import call_async, EventPattern +import constants as cs + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + self_handle = conn.GetSelfHandle() + + jids = ['foo@bar.com', 'truc@cafe.fr'] + call_async(q, conn, 'RequestHandles', 1, jids) + + event = q.expect('dbus-return', method='RequestHandles') + handles = event.value[0] + + properties = conn.GetAll( + cs.CONN_IFACE_REQUESTS, dbus_interface=cs.PROPERTIES_IFACE) + assert properties.get('Channels') == [], properties['Channels'] + assert ({cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + }, + [cs.TARGET_HANDLE, cs.TARGET_ID], + ) in properties.get('RequestableChannelClasses'),\ + properties['RequestableChannelClasses'] + + test_ensure_ensure(q, conn, self_handle, jids[0], handles[0]) + test_request_ensure(q, conn, self_handle, jids[1], handles[1]) + +def test_ensure_ensure(q, conn, self_handle, jid, handle): + """ + Test ensuring a non-existant channel twice. The first call should succeed + with Yours=True; the subsequent call should succeed with Yours=False + """ + + # Check that Ensuring a channel that doesn't exist succeeds + call_async(q, conn.Requests, 'EnsureChannel', request_props (handle)) + + ret, old_sig, new_sig = q.expect_many( + EventPattern('dbus-return', method='EnsureChannel'), + EventPattern('dbus-signal', signal='NewChannel'), + EventPattern('dbus-signal', signal='NewChannels'), + ) + + assert len(ret.value) == 3 + yours, path, emitted_props = ret.value + + # The channel was created in response to the call, and we were the only + # requestor, so we should get Yours=True + assert yours, ret.value + + check_props(emitted_props, self_handle, handle, jid) + + assert len(old_sig.args) == 5 + old_path, old_ct, old_ht, old_h, old_sh = old_sig.args + + assert old_path == path + assert old_ct == cs.CHANNEL_TYPE_TEXT + assert old_ht == cs.HT_CONTACT + assert old_h == handle + assert old_sh == True # suppress handler + + assert len(new_sig.args) == 1 + assert len(new_sig.args[0]) == 1 # one channel + assert len(new_sig.args[0][0]) == 2 # two struct members + assert new_sig.args[0][0][0] == path + assert new_sig.args[0][0][1] == emitted_props + + properties = conn.GetAll( + cs.CONN_IFACE_REQUESTS, dbus_interface=dbus.PROPERTIES_IFACE) + + assert new_sig.args[0][0] in properties['Channels'], \ + (new_sig.args[0][0], properties['Channels']) + + + # Now try Ensuring a channel which already exists + call_async(q, conn.Requests, 'EnsureChannel', request_props(handle)) + ret_ = q.expect('dbus-return', method='EnsureChannel') + + assert len(ret_.value) == 3 + yours_, path_, emitted_props_ = ret_.value + + # Someone's already responsible for this channel, so we should get + # Yours=False + assert not yours_, ret_.value + assert path == path_, (path, path_) + assert emitted_props == emitted_props_, (emitted_props, emitted_props_) + + +def test_request_ensure(q, conn, self_handle, jid, handle): + """ + Test Creating a non-existant channel, then Ensuring the same channel. + The call to Ensure should succeed with Yours=False. + """ + + call_async(q, conn.Requests, 'CreateChannel', request_props(handle)) + + ret, old_sig, new_sig = q.expect_many( + EventPattern('dbus-return', method='CreateChannel'), + EventPattern('dbus-signal', signal='NewChannel'), + EventPattern('dbus-signal', signal='NewChannels'), + ) + + assert len(ret.value) == 2 + path, emitted_props = ret.value + + check_props(emitted_props, self_handle, handle, jid) + + assert len(old_sig.args) == 5 + old_path, old_ct, old_ht, old_h, old_sh = old_sig.args + + assert old_path == path + assert old_ct == cs.CHANNEL_TYPE_TEXT + assert old_ht == cs.HT_CONTACT + assert old_h == handle + assert old_sh == True # suppress handler + + assert len(new_sig.args) == 1 + assert len(new_sig.args[0]) == 1 # one channel + assert len(new_sig.args[0][0]) == 2 # two struct members + assert new_sig.args[0][0][0] == path + assert new_sig.args[0][0][1] == emitted_props + + properties = conn.GetAll( + cs.CONN_IFACE_REQUESTS, dbus_interface=dbus.PROPERTIES_IFACE) + + assert new_sig.args[0][0] in properties['Channels'], \ + (new_sig.args[0][0], properties['Channels']) + + + # Now try Ensuring that same channel. + call_async(q, conn.Requests, 'EnsureChannel', request_props(handle)) + ret_ = q.expect('dbus-return', method='EnsureChannel') + + assert len(ret_.value) == 3 + yours_, path_, emitted_props_ = ret_.value + + # Someone's already responsible for this channel, so we should get + # Yours=False + assert not yours_, ret_.value + assert path == path_, (path, path_) + assert emitted_props == emitted_props_, (emitted_props, emitted_props_) + + +def check_props(props, self_handle, handle, jid): + assert props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_TEXT + assert props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT + assert props[cs.TARGET_HANDLE] == handle + assert props[cs.TARGET_ID] == jid + assert props[cs.REQUESTED] == True + assert props[cs.INITIATOR_HANDLE] == self_handle + assert props[cs.INITIATOR_ID] == 'test@localhost' + + +def request_props(handle): + return { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_HANDLE: handle, + } + + +if __name__ == '__main__': + exec_test(test) + diff --git a/tests/twisted/text/initiate-requestotron.py b/tests/twisted/text/initiate-requestotron.py new file mode 100644 index 0000000..75ef7c0 --- /dev/null +++ b/tests/twisted/text/initiate-requestotron.py @@ -0,0 +1,72 @@ +""" +Test text channel initiated by me, using Requests. +""" + +import dbus + +from gabbletest import exec_test +from servicetest import call_async, EventPattern +import constants as cs + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + self_handle = conn.GetSelfHandle() + + jid = 'foo@bar.com' + foo_handle = conn.RequestHandles(cs.HT_CONTACT, [jid])[0] + + properties = conn.GetAll( + cs.CONN_IFACE_REQUESTS, dbus_interface=dbus.PROPERTIES_IFACE) + assert properties.get('Channels') == [], properties['Channels'] + assert ({cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + }, + [cs.TARGET_HANDLE, cs.TARGET_ID], + ) in properties.get('RequestableChannelClasses'),\ + properties['RequestableChannelClasses'] + + call_async(q, conn.Requests, 'CreateChannel', + { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_HANDLE: foo_handle, + }) + + ret, old_sig, new_sig = q.expect_many( + EventPattern('dbus-return', method='CreateChannel'), + EventPattern('dbus-signal', signal='NewChannel'), + EventPattern('dbus-signal', signal='NewChannels'), + ) + + assert len(ret.value) == 2 + emitted_props = ret.value[1] + assert emitted_props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_TEXT + assert emitted_props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT + assert emitted_props[cs.TARGET_HANDLE] == foo_handle + assert emitted_props[cs.TARGET_ID] == jid + assert emitted_props[cs.REQUESTED] == True + assert emitted_props[cs.INITIATOR_HANDLE] == self_handle + assert emitted_props[cs.INITIATOR_ID] == 'test@localhost' + + assert old_sig.args[0] == ret.value[0] + assert old_sig.args[1] == cs.CHANNEL_TYPE_TEXT + assert old_sig.args[2] == cs.HT_CONTACT + assert old_sig.args[3] == foo_handle + assert old_sig.args[4] == True # suppress handler + + assert len(new_sig.args) == 1 + assert len(new_sig.args[0]) == 1 # one channel + assert len(new_sig.args[0][0]) == 2 # two struct members + assert new_sig.args[0][0][0] == ret.value[0] + assert new_sig.args[0][0][1] == ret.value[1] + + properties = conn.GetAll( + cs.CONN_IFACE_REQUESTS, dbus_interface=dbus.PROPERTIES_IFACE) + + assert new_sig.args[0][0] in properties['Channels'], \ + (new_sig.args[0][0], properties['Channels']) + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/text/initiate.py b/tests/twisted/text/initiate.py new file mode 100644 index 0000000..dae48e5 --- /dev/null +++ b/tests/twisted/text/initiate.py @@ -0,0 +1,93 @@ +""" +Test text channel initiated by me. +""" + +import dbus + +from twisted.words.xish import domish + +from gabbletest import exec_test +from servicetest import call_async, EventPattern +import constants as cs + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + self_handle = conn.GetSelfHandle() + + jid = 'foo@bar.com' + call_async(q, conn, 'RequestHandles', cs.HT_CONTACT, [jid]) + + event = q.expect('dbus-return', method='RequestHandles') + foo_handle = event.value[0][0] + + call_async(q, conn, 'RequestChannel', + cs.CHANNEL_TYPE_TEXT, cs.HT_CONTACT, foo_handle, True) + + ret, sig = q.expect_many( + EventPattern('dbus-return', method='RequestChannel'), + EventPattern('dbus-signal', signal='NewChannel'), + ) + + text_chan = bus.get_object(conn.bus_name, ret.value[0]) + + assert sig.args[0] == ret.value[0], \ + (sig.args[0], ret.value[0]) + assert sig.args[1] == cs.CHANNEL_TYPE_TEXT, sig.args[1] + # check that handle type == contact handle + assert sig.args[2] == 1, sig.args[1] + assert sig.args[3] == foo_handle, (sig.args[3], foo_handle) + assert sig.args[4] == True # suppress handler + + # Exercise basic Channel Properties from spec 0.17.7 + channel_props = text_chan.GetAll( + cs.CHANNEL, dbus_interface=dbus.PROPERTIES_IFACE) + assert channel_props.get('TargetHandle') == foo_handle,\ + (channel_props.get('TargetHandle'), foo_handle) + assert channel_props.get('TargetHandleType') == 1,\ + channel_props.get('TargetHandleType') + assert channel_props.get('ChannelType') == \ + cs.CHANNEL_TYPE_TEXT,\ + channel_props.get('ChannelType') + assert cs.CHANNEL_IFACE_CHAT_STATE in \ + channel_props.get('Interfaces', ()), \ + channel_props.get('Interfaces') + assert channel_props['TargetID'] == jid,\ + (channel_props['TargetID'], jid) + assert channel_props['Requested'] == True + assert channel_props['InitiatorHandle'] == self_handle,\ + (channel_props['InitiatorHandle'], self_handle) + assert channel_props['InitiatorID'] == 'test@localhost',\ + channel_props['InitiatorID'] + + dbus.Interface(text_chan, cs.CHANNEL_TYPE_TEXT).Send(0, 'hey') + + event = q.expect('stream-message') + + elem = event.stanza + assert elem.name == 'message' + assert elem['type'] == 'chat' + body = list(event.stanza.elements())[0] + assert body.name == 'body' + assert body.children[0] == u'hey' + + # <message type="chat"><body>hello</body</message> + m = domish.Element((None, 'message')) + m['from'] = 'foo@bar.com/Pidgin' + m['type'] = 'chat' + m.addElement('body', content='hello') + stream.send(m) + + event = q.expect('dbus-signal', signal='Received') + + # message type: normal + assert event.args[3] == 0 + # flags: none + assert event.args[4] == 0 + # body + assert event.args[5] == 'hello' + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/text/respawn.py b/tests/twisted/text/respawn.py new file mode 100644 index 0000000..55ceb11 --- /dev/null +++ b/tests/twisted/text/respawn.py @@ -0,0 +1,172 @@ +""" +Test text channel being recreated because there are still pending messages. +""" + +import dbus + +from twisted.words.xish import domish + +from gabbletest import exec_test +from servicetest import call_async, EventPattern +import constants as cs + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + self_handle = conn.GetSelfHandle() + + jid = 'foo@bar.com' + foo_handle = conn.RequestHandles(cs.HT_CONTACT, [jid])[0] + + call_async(q, conn, 'RequestChannel', + cs.CHANNEL_TYPE_TEXT, cs.HT_CONTACT, foo_handle, True) + + ret, old_sig, new_sig = q.expect_many( + EventPattern('dbus-return', method='RequestChannel'), + EventPattern('dbus-signal', signal='NewChannel'), + EventPattern('dbus-signal', signal='NewChannels'), + ) + + text_chan = bus.get_object(conn.bus_name, ret.value[0]) + chan_iface = dbus.Interface(text_chan, cs.CHANNEL) + text_iface = dbus.Interface(text_chan, cs.CHANNEL_TYPE_TEXT) + + assert old_sig.args[0] == ret.value[0] + assert old_sig.args[1] == cs.CHANNEL_TYPE_TEXT + assert old_sig.args[2] == cs.HT_CONTACT + assert old_sig.args[3] == foo_handle + assert old_sig.args[4] == True # suppress handler + + assert len(new_sig.args) == 1 + assert len(new_sig.args[0]) == 1 # one channel + assert len(new_sig.args[0][0]) == 2 # two struct members + assert new_sig.args[0][0][0] == ret.value[0] + emitted_props = new_sig.args[0][0][1] + assert emitted_props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_TEXT + assert emitted_props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT + assert emitted_props[cs.TARGET_HANDLE] == foo_handle + assert emitted_props[cs.TARGET_ID] == jid + assert emitted_props[cs.REQUESTED] == True + assert emitted_props[cs.INITIATOR_HANDLE] == self_handle + assert emitted_props[cs.INITIATOR_ID] == 'test@localhost' + + channel_props = text_chan.GetAll( + cs.CHANNEL, dbus_interface=dbus.PROPERTIES_IFACE) + assert channel_props['TargetID'] == jid,\ + (channel_props['TargetID'], jid) + assert channel_props['Requested'] == True + assert channel_props['InitiatorHandle'] == self_handle,\ + (channel_props['InitiatorHandle'], self_handle) + assert channel_props['InitiatorID'] == 'test@localhost',\ + channel_props['InitiatorID'] + + text_iface.Send(0, 'hey') + + event = q.expect('stream-message') + + elem = event.stanza + assert elem.name == 'message' + assert elem['type'] == 'chat' + body = list(event.stanza.elements())[0] + assert body.name == 'body' + assert body.children[0] == u'hey' + + # <message type="chat"><body>hello</body</message> + m = domish.Element((None, 'message')) + m['from'] = 'foo@bar.com/Pidgin' + m['type'] = 'chat' + m.addElement('body', content='hello') + stream.send(m) + + event = q.expect('dbus-signal', signal='Received') + + hello_message_id = event.args[0] + hello_message_time = event.args[1] + assert event.args[2] == foo_handle + # message type: normal + assert event.args[3] == 0 + # flags: none + assert event.args[4] == 0 + # body + assert event.args[5] == 'hello' + + messages = text_chan.ListPendingMessages(False, + dbus_interface=cs.CHANNEL_TYPE_TEXT) + assert messages == \ + [(hello_message_id, hello_message_time, foo_handle, + 0, 0, 'hello')], messages + + # close the channel without acking the message; it comes back + + call_async(q, chan_iface, 'Close') + + old, new = q.expect_many( + EventPattern('dbus-signal', signal='Closed'), + EventPattern('dbus-signal', signal='ChannelClosed'), + ) + assert old.path == text_chan.object_path,\ + (old.path, text_chan.object_path) + assert new.args[0] == text_chan.object_path,\ + (new.args[0], text_chan.object_path) + + event = q.expect('dbus-signal', signal='NewChannel') + assert event.args[0] == text_chan.object_path + assert event.args[1] == cs.CHANNEL_TYPE_TEXT + assert event.args[2] == cs.HT_CONTACT + assert event.args[3] == foo_handle + assert event.args[4] == False # suppress handler + + event = q.expect('dbus-return', method='Close') + + # it now behaves as if the message had initiated it + + channel_props = text_chan.GetAll( + cs.CHANNEL, dbus_interface=dbus.PROPERTIES_IFACE) + assert channel_props['TargetID'] == jid,\ + (channel_props['TargetID'], jid) + assert channel_props['Requested'] == False + assert channel_props['InitiatorHandle'] == foo_handle,\ + (channel_props['InitiatorHandle'], foo_handle) + assert channel_props['InitiatorID'] == 'foo@bar.com',\ + channel_props['InitiatorID'] + + # the message is still there + + messages = text_chan.ListPendingMessages(False, + dbus_interface=cs.CHANNEL_TYPE_TEXT) + assert messages == \ + [(hello_message_id, hello_message_time, foo_handle, + 0, 8, 'hello')], messages + + # acknowledge it + + text_chan.AcknowledgePendingMessages([hello_message_id], + dbus_interface=cs.CHANNEL_TYPE_TEXT) + + messages = text_chan.ListPendingMessages(False, + dbus_interface=cs.CHANNEL_TYPE_TEXT) + assert messages == [] + + # close the channel again + + call_async(q, chan_iface, 'Close') + + event = q.expect('dbus-signal', signal='Closed') + assert event.path == text_chan.object_path,\ + (event.path, text_chan.object_path) + + event = q.expect('dbus-return', method='Close') + + # assert that it stays dead this time! + + try: + chan_iface.GetChannelType() + except dbus.DBusException: + pass + else: + raise AssertionError("Why won't it die?") + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/text/send-error.py b/tests/twisted/text/send-error.py new file mode 100644 index 0000000..039223c --- /dev/null +++ b/tests/twisted/text/send-error.py @@ -0,0 +1,184 @@ +""" +Test that an incoming <message><error/></> for a contact gives both a SendError +and a delivery report on a 1-1 text channel to that contact. +""" + +from twisted.words.xish import domish + +from gabbletest import exec_test +from servicetest import call_async, EventPattern +import constants as cs +import ns + +def test_temporary_error(q, bus, conn, stream): + self_handle = conn.GetSelfHandle() + + jid = 'foo@bar.com' + call_async(q, conn, 'RequestHandles', 1, [jid]) + + event = q.expect('dbus-return', method='RequestHandles') + foo_handle = event.value[0][0] + + path = conn.Requests.CreateChannel( + { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_HANDLE: foo_handle, + })[0] + text_chan = bus.get_object(conn.bus_name, path) + + # <message from='foo@bar.com' type='error'> + # <body>what is up, my good sir?</body> + # <error type='wait'> + # <resource-constraint xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> + # </error> + # </message> + message_body = 'what is up, my good sir?' + + m = domish.Element((None, 'message')) + m['from'] = 'foo@bar.com' + m['id'] = '1845a1a9-f7bc-4a2e-a885-633aadc81e1b' + m['type'] = 'error' + m.addElement('body', content=message_body) + + e = domish.Element((None, 'error')) + e['type'] = 'wait' + e.addElement((ns.STANZA, 'resource-constraint')) + + m.addChild(e) + + stream.send(m) + + send_error, received, message_received = q.expect_many( + EventPattern('dbus-signal', signal='SendError'), + EventPattern('dbus-signal', signal='Received'), + EventPattern('dbus-signal', signal='MessageReceived'), + ) + + expected_send_error = 4 # Too_Long + + assert send_error.args[0] == expected_send_error, send_error.args + # FIXME: It doesn't look like it's possible to know what the original + # message type is, given that the type attribute of <message> is 'error' + # for error reports. + #assert send_error.args[2] == 0, send_error.args + assert send_error.args[3] == message_body, send_error.args + + assert received.args[2] == foo_handle, (received.args, foo_handle) + assert received.args[3] == 4, received.args # Channel_Text_Message_Type_Delivery_Report + assert received.args[4] == 2, received.args # Channel_Text_Message_Flag_Non_Text_Content + assert received.args[5] == '', received.args + + delivery_report = message_received.args[0] + assert len(delivery_report) == 1, delivery_report + header = delivery_report[0] + assert header['message-sender'] == foo_handle, header + assert header['message-type'] == 4, header # Channel_Text_Message_Type_Delivery_Report + assert header['delivery-status'] == 2, header # Delivery_Status_Temporarily_Failed + assert header['delivery-token'] == '1845a1a9-f7bc-4a2e-a885-633aadc81e1b',\ + header + assert header['delivery-error'] == expected_send_error, header + + delivery_echo = header['delivery-echo'] + assert len(delivery_echo) == 2, delivery_echo + + assert delivery_echo[0]['message-sender'] == self_handle, delivery_echo + assert delivery_echo[0]['message-token'] == \ + '1845a1a9-f7bc-4a2e-a885-633aadc81e1b', delivery_echo + # FIXME: see above + #assert delivery_echo[0]['message-type'] == 0, delivery_echo + + assert delivery_echo[1]['content-type'] == "text/plain", delivery_echo + assert delivery_echo[1]['content'] == message_body, delivery_echo + + +def test_permanent_error(q, bus, conn, stream): + self_handle = conn.GetSelfHandle() + + jid = 'wee@ninja.jp' + call_async(q, conn, 'RequestHandles', 1, [jid]) + + event = q.expect('dbus-return', method='RequestHandles') + ninja_handle = event.value[0][0] + + path = conn.Requests.CreateChannel( + { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_HANDLE: ninja_handle, + })[0] + text_chan = bus.get_object(conn.bus_name, path) + + # <message from='wee@ninja.jp' type='error'> + # <body>hello? is there anyone there?</body> + # <error type='cancel'> + # <item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> + # </error> + # </message> + message_body = 'hello? is there anyone there?' + + m = domish.Element((None, 'message')) + m['from'] = 'wee@ninja.jp' + m['type'] = 'error' + m.addElement('body', content=message_body) + + e = domish.Element((None, 'error')) + e['type'] = 'cancel' + e.addElement((ns.STANZA, 'item-not-found')) + + m.addChild(e) + + stream.send(m) + + send_error, received, message_received = q.expect_many( + EventPattern('dbus-signal', signal='SendError'), + EventPattern('dbus-signal', signal='Received'), + EventPattern('dbus-signal', signal='MessageReceived'), + ) + + expected_send_error = 2 # Invalid_Contact + + assert send_error.args[0] == expected_send_error, send_error.args + # FIXME: It doesn't look like it's possible to know what the original + # message type is, given that the type attribute of <message> is 'error' + # for error reports. + #assert send_error.args[2] == 0, send_error.args + assert send_error.args[3] == message_body, send_error.args + + assert received.args[2] == ninja_handle, (received.args, ninja_handle) + assert received.args[3] == 4, received.args # Channel_Text_Message_Type_Delivery_Report + assert received.args[4] == 2, received.args # Channel_Text_Message_Flag_Non_Text_Content + assert received.args[5] == '', received.args + + delivery_report = message_received.args[0] + assert len(delivery_report) == 1, delivery_report + header = delivery_report[0] + assert header['message-sender'] == ninja_handle, header + assert header['message-type'] == 4, header # Channel_Text_Message_Type_Delivery_Report + assert header['delivery-status'] == 3, header # Delivery_Status_Permanently_Failed + # the error has no ID, therefore its Telepathy rendition has no + # delivery-token + assert 'delivery-token' not in header, header + assert header['delivery-error'] == expected_send_error, header + + delivery_echo = header['delivery-echo'] + assert len(delivery_echo) == 2, delivery_echo + + assert delivery_echo[0]['message-sender'] == self_handle, delivery_echo + # the error has no ID, therefore the echo's Telepathy rendition has no + # message-token + assert 'message-token' not in delivery_echo[0], delivery_echo + # FIXME: see above + #assert delivery_echo[0]['message-type'] == 0, delivery_echo + + assert delivery_echo[1]['content-type'] == "text/plain", delivery_echo + assert delivery_echo[1]['content'] == message_body, delivery_echo + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + test_temporary_error(q, bus, conn, stream) + test_permanent_error(q, bus, conn, stream) + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/text/send-to-correct-resource.py b/tests/twisted/text/send-to-correct-resource.py new file mode 100644 index 0000000..4668d8a --- /dev/null +++ b/tests/twisted/text/send-to-correct-resource.py @@ -0,0 +1,79 @@ +""" +Regression test for https://bugs.freedesktop.org/show_bug.cgi?id=22369. +""" + +from twisted.words.xish import domish + +from servicetest import wrap_channel +from gabbletest import exec_test +import constants as cs +import ns + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + # <wjt> I need a random name generator + # <fledermaus> Macro-Variable Spin Gel + contact = 'macro-variable.spin.gel@example.com' + contact_a = '%s/n810' % contact + contact_b = '%s/laptop' % contact + + path, _ = conn.Requests.CreateChannel({ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_ID: contact, + }) + chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text') + + # When we start a conversation, Gabble should send to the bare JID. + chan.Text.Send(0, 'hey, you around?') + q.expect('stream-message', to=contact) + + # A particular resource replies. + m = domish.Element((None, 'message')) + m['from'] = contact_a + m['type'] = 'chat' + m.addElement('body', content="i'm on a beach at Gran Canaria!") + stream.send(m) + + q.expect('dbus-signal', signal='Received') + + # Now that we got a reply from a particular resource, Gabble should reply + # there. + chan.Text.Send(0, 'nice') + q.expect('stream-message', to=contact_a) + + # Now another resource messages us + m = domish.Element((None, 'message')) + m['from'] = contact_b + m['type'] = 'chat' + m.addElement('body', content="I brought my laptop to the Empathy hackfest") + stream.send(m) + + q.expect('dbus-signal', signal='Received') + + # Gabble should have updated the resource it's sending to. + chan.Text.Send(0, "don't get sand in the keyboard") + e = q.expect('stream-message', to=contact_b) + + # But actually that resource has gone offline: + m = e.stanza + m['from'] = contact_b + m['type'] = 'error' + del m['to'] + + err = m.addElement((None, 'error')) + err['type'] = 'cancel' + err.addElement((ns.STANZA, 'item-not-found')) + + stream.send(m) + q.expect('dbus-signal', signal='SendError') + + # So as a result, Gabble should send the next message to the bare JID. + chan.Text.Send(0, "... i guess my warning was too late") + q.expect('stream-message', to=contact) + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/text/test-chat-state.py b/tests/twisted/text/test-chat-state.py new file mode 100644 index 0000000..289f692 --- /dev/null +++ b/tests/twisted/text/test-chat-state.py @@ -0,0 +1,303 @@ +# coding=utf-8 +""" +Test that chat state notifications are correctly sent and received on text +channels. +""" + +from twisted.words.xish import domish + +from servicetest import assertEquals, assertLength, wrap_channel, EventPattern +from gabbletest import exec_test, make_result_iq, sync_stream, make_presence +import constants as cs +import ns + +def check_state_notification(elem, name, allow_body=False): + assertEquals('message', elem.name) + assertEquals('chat', elem['type']) + + children = list(elem.elements()) + notification = [x for x in children if x.uri == ns.CHAT_STATES][0] + assert notification.name == name, notification.toXml() + + if not allow_body: + assert len(children) == 1, elem.toXml() + +def make_message(jid, body=None, state=None): + m = domish.Element((None, 'message')) + m['from'] = jid + m['type'] = 'chat' + + if state is not None: + m.addElement((ns.CHAT_STATES, state)) + + if body is not None: + m.addElement('body', content=body) + + return m + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + self_handle = conn.GetSelfHandle() + + jid = 'foo@bar.com' + full_jid = 'foo@bar.com/Foo' + foo_handle = conn.RequestHandles(cs.HT_CONTACT, [jid])[0] + + path = conn.Requests.CreateChannel( + { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_HANDLE: foo_handle, + })[0] + chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text', + ['ChatState', 'Destroyable']) + + presence = make_presence(full_jid, status='hello', + caps={ + 'node': 'http://telepathy.freedesktop.org/homeopathy', + 'ver' : '0.1', + }) + stream.send(presence) + + version_event = q.expect('stream-iq', to=full_jid, + query_ns=ns.DISCO_INFO, + query_node='http://telepathy.freedesktop.org/homeopathy#0.1') + + result = make_result_iq(stream, version_event.stanza) + query = result.firstChildElement() + feature = query.addElement('feature') + feature['var'] = ns.CHAT_STATES + stream.send(result) + + sync_stream(q, stream) + + # Receiving chat states: + + # Composing... + stream.send(make_message(full_jid, state='composing')) + + changed = q.expect('dbus-signal', signal='ChatStateChanged') + handle, state = changed.args + assertEquals(foo_handle, handle) + assertEquals(cs.CHAT_STATE_COMPOSING, state) + + # Message! + stream.send(make_message(full_jid, body='hello', state='active')) + + changed = q.expect('dbus-signal', signal='ChatStateChanged') + handle, state = changed.args + assertEquals(foo_handle, handle) + assertEquals(cs.CHAT_STATE_ACTIVE, state) + + # Sending chat states: + + # Composing... + chan.ChatState.SetChatState(cs.CHAT_STATE_COMPOSING) + + stream_message = q.expect('stream-message') + check_state_notification(stream_message.stanza, 'composing') + + # XEP 0085: + # every content message SHOULD contain an <active/> notification. + chan.Text.Send(0, 'hi.') + + stream_message = q.expect('stream-message') + elem = stream_message.stanza + assertEquals('chat', elem['type']) + + check_state_notification(elem, 'active', allow_body=True) + + def is_body(e): + if e.name == 'body': + assert e.children[0] == u'hi.', e.toXml() + return True + return False + + assert len([x for x in elem.elements() if is_body(x)]) == 1, elem.toXml() + + # Close the channel without acking the received message. The peer should + # get a <gone/> notification, and the channel should respawn. + chan.Close() + + gone, _, _ = q.expect_many( + EventPattern('stream-message'), + EventPattern('dbus-signal', signal='Closed'), + EventPattern('dbus-signal', signal='NewChannel'), + ) + check_state_notification(gone.stanza, 'gone') + + # Reusing the proxy object because we happen to know it'll be at the same + # path... + + # Destroy the channel. The peer shouldn't get a <gone/> notification, since + # we already said we were gone and haven't sent them any messages to the + # contrary. + es = [EventPattern('stream-message')] + q.forbid_events(es) + + chan.Destroyable.Destroy() + sync_stream(q, stream) + + # Make the channel anew. + path = conn.Requests.CreateChannel( + { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_HANDLE: foo_handle, + })[0] + chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text', + ['ChatState', 'Destroyable']) + + # Close it immediately; the peer should again not get a <gone/> + # notification, since we haven't sent any notifications on that channel. + chan.Close() + sync_stream(q, stream) + q.unforbid_events(es) + + # XEP-0085 §5.1 defines how to negotiate support for chat states with a + # contact in the absence of capabilities. This is useful when talking to + # invisible contacts, for example. + + # First, if we receive a message from a contact, containing an <active/> + # notification, they support chat states, so we should send them. + + jid = 'i@example.com' + full_jid = jid + '/GTalk' + + path = conn.Requests.CreateChannel( + { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_ID: jid, + })[0] + chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text', + ['ChatState']) + + stream.send(make_message(full_jid, body='i am invisible', state='active')) + + changed = q.expect('dbus-signal', signal='ChatStateChanged') + assertEquals(cs.CHAT_STATE_ACTIVE, changed.args[1]) + + # We've seen them send a chat state notification, so we should send them + # notifications when the UI tells us to. + chan.ChatState.SetChatState(cs.CHAT_STATE_COMPOSING) + stream_message = q.expect('stream-message', to=full_jid) + check_state_notification(stream_message.stanza, 'composing') + + chan.Text.Send(0, 'very convincing') + stream_message = q.expect('stream-message', to=full_jid) + check_state_notification(stream_message.stanza, 'active', allow_body=True) + + # Now, test the case where we start the negotiation, and the contact + # turns out to support chat state notifications. + + jid = 'c@example.com' + full_jid = jid + '/GTalk' + path = conn.Requests.CreateChannel( + { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_ID: jid, + })[0] + chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text', + ['ChatState']) + + # We shouldn't send any notifications until we actually send a message. + e = EventPattern('stream-message', to=jid) + q.forbid_events([e]) + for i in [cs.CHAT_STATE_COMPOSING, cs.CHAT_STATE_INACTIVE, + cs.CHAT_STATE_PAUSED, cs.CHAT_STATE_ACTIVE]: + chan.ChatState.SetChatState(i) + sync_stream(q, stream) + q.unforbid_events([e]) + + # When we send a message, say we're active. + chan.Text.Send(0, 'is anyone there?') + stream_message = q.expect('stream-message', to=jid) + check_state_notification(stream_message.stanza, 'active', allow_body=True) + + # We get a notification back from our contact. + stream.send(make_message(full_jid, state='composing')) + + changed = q.expect('dbus-signal', signal='ChatStateChanged') + _, state = changed.args + assertEquals(cs.CHAT_STATE_COMPOSING, state) + + # So now we know they support notification, so should send notifications. + chan.ChatState.SetChatState(cs.CHAT_STATE_COMPOSING) + + # This doesn't check whether we're sending to the bare jid, or the + # jid+resource. In fact, the notification is sent to the bare jid, because + # we only update which jid we send to when we actually receive a message, + # not when we receive a notification. wjt thinks this is less surprising + # than the alternative: + # + # • I'm talking to you on my N900, and signed in on my laptop; + # • I enter one character in a tab to you on my laptop, and then delete + # it; + # • Now your messages to me appear on my laptop (until I send you another + # one from my N900)! + stream_message = q.expect('stream-message') + check_state_notification(stream_message.stanza, 'composing') + + # But! Now they start messaging us from a different client, which *doesn't* + # support notifications. + other_jid = jid + '/Library' + stream.send(make_message(other_jid, body='grr, library computers')) + q.expect('dbus-signal', signal='Received') + + # Okay, we should stop sending typing notifications. + e = EventPattern('stream-message', to=other_jid) + q.forbid_events([e]) + for i in [cs.CHAT_STATE_COMPOSING, cs.CHAT_STATE_INACTIVE, + cs.CHAT_STATE_PAUSED, cs.CHAT_STATE_ACTIVE]: + chan.ChatState.SetChatState(i) + sync_stream(q, stream) + q.unforbid_events([e]) + + # Now, test the case where we start the negotiation, and the contact + # does not support chat state notifications + + jid = 'twitterbot@example.com' + full_jid = jid + '/Nonsense' + path = conn.Requests.CreateChannel( + { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_ID: jid, + })[0] + chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text', + ['ChatState']) + + # We shouldn't send any notifications until we actually send a message. + e = EventPattern('stream-message', to=jid) + q.forbid_events([e]) + for i in [cs.CHAT_STATE_COMPOSING, cs.CHAT_STATE_INACTIVE, + cs.CHAT_STATE_PAUSED, cs.CHAT_STATE_ACTIVE]: + chan.ChatState.SetChatState(i) + sync_stream(q, stream) + q.unforbid_events([e]) + + # When we send a message, say we're active. + chan.Text.Send(0, '#n900 #maemo #zomg #woo #yay http://bit.ly/n900') + stream_message = q.expect('stream-message', to=jid) + check_state_notification(stream_message.stanza, 'active', allow_body=True) + + # They reply without a chat state. + stream.send(make_message(full_jid, body="posted.")) + q.expect('dbus-signal', signal='Received') + + # Okay, we shouldn't send any more. + e = EventPattern('stream-message', to=other_jid) + q.forbid_events([e]) + for i in [cs.CHAT_STATE_COMPOSING, cs.CHAT_STATE_INACTIVE, + cs.CHAT_STATE_PAUSED, cs.CHAT_STATE_ACTIVE]: + chan.ChatState.SetChatState(i) + sync_stream(q, stream) + q.unforbid_events([e]) + + chan.Text.Send(0, '@stephenfry simmer down') + message = q.expect('stream-message') + states = [x for x in message.stanza.elements() if x.uri == ns.CHAT_STATES] + assertLength(0, states) + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/text/test-text-delayed.py b/tests/twisted/text/test-text-delayed.py new file mode 100644 index 0000000..1a652cf --- /dev/null +++ b/tests/twisted/text/test-text-delayed.py @@ -0,0 +1,56 @@ + +""" +Test receiving delayed (offline) messages on a text channel. +""" + +import datetime + +from twisted.words.xish import domish + +from gabbletest import exec_test +from servicetest import EventPattern +import constants as cs + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + m = domish.Element((None, 'message')) + m['from'] = 'foo@bar.com' + m['type'] = 'chat' + m.addElement('body', content='hello') + + # add timestamp information + x = m.addElement(('jabber:x:delay', 'x')) + x['stamp'] = '20070517T16:15:01' + + stream.send(m) + + event = q.expect('dbus-signal', signal='NewChannel') + assert event.args[1] == cs.CHANNEL_TYPE_TEXT + assert event.args[2] == cs.HT_CONTACT + jid = conn.InspectHandles(cs.HT_CONTACT, [event.args[3]])[0] + assert jid == 'foo@bar.com' + + received, message_received = q.expect_many( + EventPattern('dbus-signal', signal='Received'), + EventPattern('dbus-signal', signal='MessageReceived'), + ) + + assert (str(datetime.datetime.utcfromtimestamp(received.args[1])) + == '2007-05-17 16:15:01') + assert received.args[5] == 'hello' + + message = message_received.args[0] + header = message[0] + message_sent_timestamp = header['message-sent'] + assert str(datetime.datetime.utcfromtimestamp(message_sent_timestamp) + == '2007-05-17 16:15:01'), header + message_received_timestamp = header['message-received'] + assert message_received_timestamp > message_sent_timestamp, header + + assert message[1]['content'] == 'hello', message + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/text/test-text-no-body.py b/tests/twisted/text/test-text-no-body.py new file mode 100644 index 0000000..09ae6f4 --- /dev/null +++ b/tests/twisted/text/test-text-no-body.py @@ -0,0 +1,40 @@ + +""" +Test that <message>s with a chat state notification but no body don't create a +new text channel. +""" + +from twisted.words.xish import domish + +from gabbletest import exec_test +import constants as cs +import ns + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + # message without body + m = domish.Element((None, 'message')) + m['from'] = 'alice@foo.com' + m['type'] = 'chat' + m.addElement((ns.CHAT_STATES, 'composing')) + stream.send(m) + + # message with body + m = domish.Element((None, 'message')) + m['from'] = 'bob@foo.com' + m['type'] = 'chat' + m.addElement((ns.CHAT_STATES, 'active')) + m.addElement('body', content='hello') + stream.send(m) + + # first message should be from Bob, not Alice + event = q.expect('dbus-signal', signal='NewChannel') + assert event.args[1] == cs.CHANNEL_TYPE_TEXT + jid = conn.InspectHandles(cs.HT_CONTACT, [event.args[3]])[0] + assert jid == 'bob@foo.com' + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/text/test-text.py b/tests/twisted/text/test-text.py new file mode 100644 index 0000000..e685429 --- /dev/null +++ b/tests/twisted/text/test-text.py @@ -0,0 +1,177 @@ + +""" +Test text channel. +""" + +import dbus + +from twisted.words.xish import domish + +from gabbletest import exec_test +from servicetest import EventPattern, wrap_channel, assertNotEquals +import constants as cs + +def test(q, bus, conn, stream): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + id = '1845a1a9-f7bc-4a2e-a885-633aadc81e1b' + + # <message type="chat"><body>hello</body</message> + m = domish.Element((None, 'message')) + m['from'] = 'foo@bar.com/Pidgin' + m['id'] = id + m['type'] = 'chat' + m.addElement('body', content='hello') + stream.send(m) + + event = q.expect('dbus-signal', signal='NewChannel') + text_chan = wrap_channel( + bus.get_object(conn.bus_name, event.args[0]), 'Text', ['Messages']) + assert event.args[1] == cs.CHANNEL_TYPE_TEXT + assert event.args[2] == cs.HT_CONTACT + foo_at_bar_dot_com_handle = event.args[3] + jid = conn.InspectHandles(1, [foo_at_bar_dot_com_handle])[0] + assert jid == 'foo@bar.com' + assert event.args[4] == False # suppress handler + + # Exercise basic Channel Properties from spec 0.17.7 + channel_props = text_chan.Properties.GetAll(cs.CHANNEL) + assert channel_props.get('TargetHandle') == event.args[3],\ + (channel_props.get('TargetHandle'), event.args[3]) + assert channel_props.get('TargetHandleType') == cs.HT_CONTACT,\ + channel_props.get('TargetHandleType') + assert channel_props.get('ChannelType') == \ + cs.CHANNEL_TYPE_TEXT,\ + channel_props.get('ChannelType') + assert cs.CHANNEL_IFACE_CHAT_STATE in \ + channel_props.get('Interfaces', ()), \ + channel_props.get('Interfaces') + assert cs.CHANNEL_IFACE_MESSAGES in \ + channel_props.get('Interfaces', ()), \ + channel_props.get('Interfaces') + assert channel_props['TargetID'] == jid,\ + (channel_props['TargetID'], jid) + assert channel_props['Requested'] == False + assert channel_props['InitiatorHandle'] == event.args[3],\ + (channel_props['InitiatorHandle'], event.args[3]) + assert channel_props['InitiatorID'] == jid,\ + (channel_props['InitiatorID'], jid) + + received, message_received = q.expect_many( + EventPattern('dbus-signal', signal='Received'), + EventPattern('dbus-signal', signal='MessageReceived'), + ) + + # Check that C.T.Text.Received looks right + # message type: normal + assert received.args[3] == 0 + # flags: none + assert received.args[4] == 0 + # body + assert received.args[5] == 'hello' + + + # Check that C.I.Messages.MessageReceived looks right. + message = message_received.args[0] + + # message should have two parts: the header and one content part + assert len(message) == 2, message + header, body = message + + assert header['message-sender'] == foo_at_bar_dot_com_handle, header + # the spec says that message-type "MAY be omitted for normal chat + # messages." + assert 'message-type' not in header or header['message-type'] == 0, header + + # This looks wrong, but is correct. We don't know if our contacts generate + # message id='' attributes which are unique enough for our requirements, so + # we should not use them as the message-token for incoming messages. + assertNotEquals(id, header['message-token']) + + assert body['content-type'] == 'text/plain', body + assert body['content'] == 'hello', body + + # Remove the message from the pending message queue, and check that + # PendingMessagesRemoved fires. + message_id = header['pending-message-id'] + + text_chan.Text.AcknowledgePendingMessages([message_id]) + + removed = q.expect('dbus-signal', signal='PendingMessagesRemoved') + + removed_ids = removed.args[0] + assert len(removed_ids) == 1, removed_ids + assert removed_ids[0] == message_id, (removed_ids, message_id) + + # Send a Notice using the Messages API + greeting = [ + dbus.Dictionary({ 'message-type': 2, # Notice + }, signature='sv'), + { 'content-type': 'text/plain', + 'content': u"what up", + } + ] + + sent_token = text_chan.Messages.SendMessage(greeting, dbus.UInt32(0)) + + stream_message, sent, message_sent = q.expect_many( + EventPattern('stream-message'), + EventPattern('dbus-signal', signal='Sent'), + EventPattern('dbus-signal', signal='MessageSent'), + ) + + elem = stream_message.stanza + assert elem.name == 'message' + assert elem['type'] == 'normal' + body = list(stream_message.stanza.elements())[0] + assert body.name == 'body' + assert body.children[0] == u'what up' + + sent_message = message_sent.args[0] + assert len(sent_message) == 2, sent_message + header = sent_message[0] + assert header['message-type'] == 2, header # Notice + assert header['message-token'] == sent_token, header + body = sent_message[1] + assert body['content-type'] == 'text/plain', body + assert body['content'] == u'what up', body + + assert message_sent.args[2] == sent_token + + assert sent.args[1] == 2, sent.args # Notice + assert sent.args[2] == u'what up', sent.args + + + # Send a message using Channel.Type.Text API + text_chan.Text.Send(0, 'goodbye') + + stream_message, sent, message_sent = q.expect_many( + EventPattern('stream-message'), + EventPattern('dbus-signal', signal='Sent'), + EventPattern('dbus-signal', signal='MessageSent'), + ) + + elem = stream_message.stanza + assert elem.name == 'message' + assert elem['type'] == 'chat' + body = list(stream_message.stanza.elements())[0] + assert body.name == 'body' + assert body.children[0] == u'goodbye' + + sent_message = message_sent.args[0] + assert len(sent_message) == 2, sent_message + header = sent_message[0] + # the spec says that message-type "MAY be omitted for normal chat + # messages." + assert 'message-type' not in header or header['message-type'] == 0, header + body = sent_message[1] + assert body['content-type'] == 'text/plain', body + assert body['content'] == u'goodbye', body + + assert sent.args[1] == 0, sent.args # message type normal + assert sent.args[2] == u'goodbye', sent.args + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/tools/Makefile.am b/tests/twisted/tools/Makefile.am new file mode 100644 index 0000000..efcff37 --- /dev/null +++ b/tests/twisted/tools/Makefile.am @@ -0,0 +1,33 @@ +exec-with-log.sh: exec-with-log.sh.in + $(AM_V_GEN)sed -e "s|[@]abs_top_builddir[@]|@abs_top_builddir@|g" \ + -e "s|[@]abs_top_srcdir[@]|@abs_top_srcdir@|g" $< > $@ + @chmod +x $@ + +%.conf: %.conf.in + $(AM_V_GEN)sed -e "s|[@]abs_top_builddir[@]|@abs_top_builddir@|g" $< > $@ + +# We don't use the full filename for the .in because > 99 character filenames +# in tarballs are non-portable (and automake 1.8 doesn't let us build +# non-archaic tarballs) +org.freedesktop.Telepathy.ConnectionManager.%.service: %.service.in + $(AM_V_GEN)sed -e "s|[@]abs_top_builddir[@]|@abs_top_builddir@|g" $< > $@ + +# D-Bus service file for testing +service_in_files = gabble.service.in +service_files = org.freedesktop.Telepathy.ConnectionManager.gabble.service + +# D-Bus config file for testing +conf_in_files = tmp-session-bus.conf.in +conf_files = $(conf_in_files:.conf.in=.conf) + +BUILT_SOURCES = $(service_files) $(conf_files) exec-with-log.sh + +EXTRA_DIST = \ + $(service_in_files) \ + $(conf_in_files) \ + exec-with-log.sh.in \ + with-session-bus.sh + +CLEANFILES = \ + $(BUILT_SOURCES) \ + gabble-testing.log diff --git a/tests/twisted/tools/exec-with-log.sh b/tests/twisted/tools/exec-with-log.sh new file mode 100755 index 0000000..d443fb8 --- /dev/null +++ b/tests/twisted/tools/exec-with-log.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +cd "/home/kalfa/moblin/src/telepathy-gabble/tests/twisted/tools" + +export GABBLE_DEBUG=all LM_DEBUG=net GIBBER_DEBUG=all WOCKY_DEBUG=all +export GABBLE_TIMING=1 +export GABBLE_PLUGIN_DIR="/home/kalfa/moblin/src/telepathy-gabble/plugins/.libs" +ulimit -c unlimited +exec >> gabble-testing.log 2>&1 + +if test -n "$GABBLE_TEST_VALGRIND"; then + export G_DEBUG=${G_DEBUG:+"${G_DEBUG},"}gc-friendly + export 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=/home/kalfa/moblin/src/telepathy-gabble/tests/suppressions/tp-glib.supp" + GABBLE_WRAPPER="$GABBLE_WRAPPER --suppressions=/home/kalfa/moblin/src/telepathy-gabble/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" +fi + +export G_DEBUG=fatal-warnings,fatal-criticals" ${G_DEBUG}" +exec /home/kalfa/moblin/src/telepathy-gabble/libtool --mode=execute $GABBLE_WRAPPER ../telepathy-gabble-debug diff --git a/tests/twisted/tools/exec-with-log.sh.in b/tests/twisted/tools/exec-with-log.sh.in new file mode 100644 index 0000000..7fb1758 --- /dev/null +++ b/tests/twisted/tools/exec-with-log.sh.in @@ -0,0 +1,33 @@ +#!/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 GABBLE_PLUGIN_DIR="@abs_top_builddir@/plugins/.libs" +ulimit -c unlimited +exec >> gabble-testing.log 2>&1 + +if test -n "$GABBLE_TEST_VALGRIND"; then + export G_DEBUG=${G_DEBUG:+"${G_DEBUG},"}gc-friendly + export 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" +fi + +export G_DEBUG=fatal-warnings,fatal-criticals" ${G_DEBUG}" +exec @abs_top_builddir@/libtool --mode=execute $GABBLE_WRAPPER ../telepathy-gabble-debug diff --git a/tests/twisted/tools/failure-helper.sh b/tests/twisted/tools/failure-helper.sh new file mode 100644 index 0000000..d3e427c --- /dev/null +++ b/tests/twisted/tools/failure-helper.sh @@ -0,0 +1,105 @@ +#!/bin/sh +# +# Run a given test repeatedly until it fails at least once and passes at least +# once. Places logs from the test and from the service being tested in the +# given directory, and generates diffs between the fail and pass cases. + +error() +{ + echo "$@" >&2 + exit 1 +} + +abspath() +{ + if echo "$1" | grep -q "^/"; then + echo "$1" + else + echo "$PWD/$1" + fi +} + +stripdiff() +{ + a=`mktemp` + b=`mktemp` + python ../../tools/log-strip.py < "$1" > "$a" + python ../../tools/log-strip.py < "$2" > "$b" + diff -U40 "$a" "$b" + rm "$a" "$b" +} + +prog=gabble +test_name="$1" +log_dir="$2" + +usage="usage: $0 test-name log-directory" +test -n "$test_name" || error "$usage" +test -n "$log_dir" || error "$usage" + +cd `dirname $0`/.. +test -f "servicetest.py" || error "can't find servicetest.py" +test -f "$test_name" || error "can't find that test" + +if ! test -d "$log_dir"; then + if ! test -e "$log_dir"; then + mkdir "$log_dir" + else + error "not a directory: $log_dir" + fi +fi + +log_dir=`abspath "$log_dir"` +test_pass_log="$log_dir/test-pass.log" +test_fail_log="$log_dir/test-fail.log" +prog_pass_log="$log_dir/$prog-pass.log" +prog_fail_log="$log_dir/$prog-fail.log" + +if test -e "$test_pass_log" -a -e "$prog_pass_log"; then + echo "using existing pass" + got_pass=true +else + got_pass=false +fi + +if test -e "$test_fail_log" -a -e "$prog_fail_log"; then + echo "using existing fail" + got_fail=true +else + got_fail=false +fi + +run=1 + +while test "$got_pass" != true -o "$got_fail" != true; do + echo -n "run $run: " + + CHECK_TWISTED_VERBOSE=1 make check-twisted TWISTED_TESTS="$test_name" \ + > "$log_dir/test.log" 2>&1 + ret=$? + + if test $ret -eq 0; then + echo "pass" + else + echo "fail" + fi + + if test $ret -eq 0 -a "$got_pass" != true; then + mv "$log_dir/test.log" "$test_pass_log" + cp "tools/$prog-testing.log" "$prog_pass_log" + got_pass=true + elif test $ret -ne 0 -a "$got_fail" != true; then + mv "$log_dir/test.log" "$test_fail_log" + cp "tools/$prog-testing.log" "$prog_fail_log" + got_fail=true + else + rm "$log_dir/test.log" + fi + + run=`expr $run + 1` +done + +stripdiff "$test_pass_log" "$test_fail_log" > "$log_dir/test-log.diff" +stripdiff "$prog_pass_log" "$prog_fail_log" > "$log_dir/$prog-log.diff" + +echo done diff --git a/tests/twisted/tools/gabble.service.in b/tests/twisted/tools/gabble.service.in new file mode 100644 index 0000000..6845c59 --- /dev/null +++ b/tests/twisted/tools/gabble.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.freedesktop.Telepathy.ConnectionManager.gabble +Exec=@abs_top_builddir@/tests/twisted/tools/exec-with-log.sh diff --git a/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service b/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service new file mode 100644 index 0000000..877cea2 --- /dev/null +++ b/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.freedesktop.Telepathy.ConnectionManager.gabble +Exec=/home/kalfa/moblin/src/telepathy-gabble/tests/twisted/tools/exec-with-log.sh diff --git a/tests/twisted/tools/tmp-session-bus.conf b/tests/twisted/tools/tmp-session-bus.conf new file mode 100644 index 0000000..828a33e --- /dev/null +++ b/tests/twisted/tools/tmp-session-bus.conf @@ -0,0 +1,30 @@ +<!-- This configuration file controls the per-user-login-session message bus. + Add a session-local.conf and edit that rather than changing this + file directly. --> + +<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN" + "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> +<busconfig> + <!-- Our well-known bus type, don't change this --> + <type>session</type> + + <listen>unix:tmpdir=/tmp</listen> + + <servicedir>/home/kalfa/moblin/src/telepathy-gabble/tests/twisted/tools</servicedir> + + <policy context="default"> + <!-- Allow everything to be sent --> + <allow send_destination="*" eavesdrop="true"/> + <!-- Allow everything to be received --> + <allow eavesdrop="true"/> + <!-- Allow anyone to own anything --> + <allow own="*"/> + </policy> + + <!-- This is included last so local configuration can override what's + in this standard file --> + + + + +</busconfig> diff --git a/tests/twisted/tools/tmp-session-bus.conf.in b/tests/twisted/tools/tmp-session-bus.conf.in new file mode 100644 index 0000000..84d8d65 --- /dev/null +++ b/tests/twisted/tools/tmp-session-bus.conf.in @@ -0,0 +1,30 @@ +<!-- This configuration file controls the per-user-login-session message bus. + Add a session-local.conf and edit that rather than changing this + file directly. --> + +<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN" + "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> +<busconfig> + <!-- Our well-known bus type, don't change this --> + <type>session</type> + + <listen>unix:tmpdir=/tmp</listen> + + <servicedir>@abs_top_builddir@/tests/twisted/tools</servicedir> + + <policy context="default"> + <!-- Allow everything to be sent --> + <allow send_destination="*" eavesdrop="true"/> + <!-- Allow everything to be received --> + <allow eavesdrop="true"/> + <!-- Allow anyone to own anything --> + <allow own="*"/> + </policy> + + <!-- This is included last so local configuration can override what's + in this standard file --> + + + + +</busconfig> diff --git a/tests/twisted/tools/with-session-bus.sh b/tests/twisted/tools/with-session-bus.sh new file mode 100644 index 0000000..9fc0be1 --- /dev/null +++ b/tests/twisted/tools/with-session-bus.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# with-session-bus.sh - run a program with a temporary D-Bus session daemon +# +# The canonical location of this program is the telepathy-glib tools/ +# directory, please synchronize any changes with that copy. +# +# Copyright (C) 2007-2008 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Copying and distribution of this file, with or without modification, +# are permitted in any medium without royalty provided the copyright +# notice and this notice are preserved. + +set -e + +me=with-session-bus + +dbus_daemon_args="--print-address=5 --print-pid=6 --fork" +sleep=0 + +usage () +{ + echo "usage: $me [options] -- program [program_options]" >&2 + echo "Requires write access to the current directory." >&2 + echo "" >&2 + echo "If \$WITH_SESSION_BUS_FORK_DBUS_MONITOR is set, fork dbus-monitor" >&2 + echo "with the arguments in \$WITH_SESSION_BUS_FORK_DBUS_MONITOR_OPT." >&2 + echo "The output of dbus-monitor is saved in $me-<pid>.dbus-monitor-logs" >&2 + exit 2 +} + +while test "z$1" != "z--"; do + case "$1" in + --sleep=*) + sleep="$1" + sleep="${sleep#--sleep=}" + shift + ;; + --session) + dbus_daemon_args="$dbus_daemon_args --session" + shift + ;; + --config-file=*) + # FIXME: assumes config file doesn't contain any special characters + dbus_daemon_args="$dbus_daemon_args $1" + shift + ;; + *) + usage + ;; + esac +done +shift +if test "z$1" = "z"; then usage; fi + +exec 5> $me-$$.address +exec 6> $me-$$.pid + +cleanup () +{ + pid=`head -n1 $me-$$.pid` + if test -n "$pid" ; then + echo "Killing temporary bus daemon: $pid" >&2 + kill -INT "$pid" + fi + rm -f $me-$$.address + rm -f $me-$$.pid +} + +trap cleanup INT HUP TERM +dbus-daemon $dbus_daemon_args + +{ echo -n "Temporary bus daemon is "; cat $me-$$.address; } >&2 +{ echo -n "Temporary bus daemon PID is "; head -n1 $me-$$.pid; } >&2 + +e=0 +DBUS_SESSION_BUS_ADDRESS="`cat $me-$$.address`" +export DBUS_SESSION_BUS_ADDRESS +# Break glass in case of emergency. +#dbus-monitor & + +if [ -n "$WITH_SESSION_BUS_FORK_DBUS_MONITOR" ] ; then + echo -n "Forking dbus-monitor $WITH_SESSION_BUS_FORK_DBUS_MONITOR_OPT" >&2 + dbus-monitor $WITH_SESSION_BUS_FORK_DBUS_MONITOR_OPT \ + &> $me-$$.dbus-monitor-logs & +fi + +"$@" || e=$? + +if test $sleep != 0; then + sleep $sleep +fi + +trap - INT HUP TERM +cleanup + +exit $e |