summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Laban <david.laban@collabora.co.uk>2011-01-31 18:23:54 +0000
committerDavid Laban <david.laban@collabora.co.uk>2011-01-31 18:24:31 +0000
commit776194c93cc447aede374f011dff5b1d49b2b0ed (patch)
treecda3b69c44c23d621986b71da8a460fff177fad8
parentabcfb59b2ae9613585515fd887f026591ffe77fd (diff)
parent2cf3c1e745c27dd48d71b05e5daa68479333885b (diff)
Merge remote branch 'alsuren/streamedmedia_tests'
Reviewed-by: Will Thompson <will.thompson@collabora.co.uk>
-rw-r--r--src/sip-media-channel.c164
-rw-r--r--tests/twisted/Makefile.am7
-rw-r--r--tests/twisted/sofiatest.py30
-rw-r--r--tests/twisted/voip/incoming-basics.py161
-rw-r--r--tests/twisted/voip/outgoing-basics.py297
-rw-r--r--tests/twisted/voip/voip_test.py172
6 files changed, 747 insertions, 84 deletions
diff --git a/src/sip-media-channel.c b/src/sip-media-channel.c
index 8f896ed..b78a57f 100644
--- a/src/sip-media-channel.c
+++ b/src/sip-media-channel.c
@@ -191,7 +191,7 @@ tpsip_media_channel_constructed (GObject *obj)
G_OBJECT_CLASS (tpsip_media_channel_parent_class);
TpDBusDaemon *bus;
TpHandleRepoIface *contact_repo;
- TpIntSet *set;
+ TpIntSet *add;
if (parent_object_class->constructed != NULL)
parent_object_class->constructed (obj);
@@ -219,16 +219,17 @@ tpsip_media_channel_constructed (GObject *obj)
g_assert (priv->initiator != 0);
tp_handle_ref (contact_repo, priv->initiator);
- set = tp_intset_new ();
- tp_intset_add (set, priv->initiator);
-
- tp_group_mixin_change_members (obj, "", set, NULL, NULL, NULL, 0, 0);
-
- tp_intset_destroy (set);
+ add = tp_intset_new_containing (priv->initiator);
+ tp_group_mixin_change_members (obj, "", add, NULL, NULL, NULL, 0, 0);
+ tp_intset_destroy (add);
- /* Allow member adding; also, we implement the 0.17.6 properties */
+ /* We start off with lots of flags, and then delete them as we work out what
+ * kind of channel we are, rather than trying to track what we need to
+ * add/remove over time. We should always have the right flags before we are
+ * advertised on the bus. */
tp_group_mixin_change_flags (obj,
- TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_PROPERTIES, 0);
+ TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_REMOVE |
+ TP_CHANNEL_GROUP_FLAG_CAN_RESCIND | TP_CHANNEL_GROUP_FLAG_PROPERTIES, 0);
}
static void tpsip_media_channel_dispose (GObject *object);
@@ -747,7 +748,14 @@ static void
tpsip_media_channel_get_handle (TpSvcChannel *iface,
DBusGMethodInvocation *context)
{
- tp_svc_channel_return_from_get_handle (context, 0, 0);
+ TpsipMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface);
+ TpsipMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self);
+
+ if (priv->handle != 0)
+ tp_svc_channel_return_from_get_handle (context, TP_HANDLE_TYPE_CONTACT,
+ priv->handle);
+ else
+ tp_svc_channel_return_from_get_handle (context, TP_HANDLE_TYPE_NONE, 0);
}
/**
@@ -1032,6 +1040,11 @@ tpsip_media_channel_create_initial_streams (TpsipMediaChannel *self)
g_assert (priv->initiator != priv->handle);
+ /* RequestChannel(None, 0) => channel is anonymous:
+ * caller uses RequestStreams to set the peer and start the call. */
+ if (priv->handle == 0)
+ return;
+
priv_outbound_call (self, priv->handle);
g_assert (priv->session != NULL);
@@ -1135,7 +1148,7 @@ tpsip_media_channel_peer_error (TpsipMediaChannel *self,
const char* message)
{
TpGroupMixin *mixin = TP_GROUP_MIXIN (self);
- TpIntSet *set;
+ TpIntSet *remove;
guint reason = TP_CHANNEL_GROUP_CHANGE_REASON_ERROR;
switch (status)
@@ -1172,12 +1185,12 @@ tpsip_media_channel_peer_error (TpsipMediaChannel *self,
if (message == NULL || !g_utf8_validate (message, -1, NULL))
message = "";
- set = tp_intset_new ();
- tp_intset_add (set, peer);
- tp_intset_add (set, mixin->self_handle);
+ remove = tp_intset_new ();
+ tp_intset_add (remove, peer);
+ tp_intset_add (remove, mixin->self_handle);
tp_group_mixin_change_members ((GObject *)self, message,
- NULL, set, NULL, NULL, peer, reason);
- tp_intset_destroy (set);
+ NULL, remove, NULL, NULL, peer, reason);
+ tp_intset_destroy (remove);
}
guint
@@ -1221,26 +1234,21 @@ priv_nua_i_bye_cb (TpsipMediaChannel *self,
{
TpsipMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self);
TpGroupMixin *mixin = TP_GROUP_MIXIN (self);
- TpIntSet *set;
+ TpIntSet *remove;
TpHandle peer;
g_return_val_if_fail (priv->session != NULL, FALSE);
peer = tpsip_media_session_get_peer (priv->session);
- set = tp_intset_new ();
- tp_intset_add (set, peer);
- tp_intset_add (set, mixin->self_handle);
+ remove = tp_intset_new ();
+ tp_intset_add (remove, peer);
+ tp_intset_add (remove, mixin->self_handle);
- tp_group_mixin_change_members ((GObject *) self,
- "",
- NULL, /* add */
- set, /* remove */
- NULL,
- NULL,
- peer,
- TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+ tp_group_mixin_change_members ((GObject *) self, "",
+ NULL, remove, NULL, NULL,
+ peer, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
- tp_intset_destroy (set);
+ tp_intset_destroy (remove);
return TRUE;
}
@@ -1253,7 +1261,7 @@ priv_nua_i_cancel_cb (TpsipMediaChannel *self,
{
TpsipMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self);
TpGroupMixin *mixin = TP_GROUP_MIXIN (self);
- TpIntSet *set;
+ TpIntSet *remove;
TpHandle actor = 0;
TpHandle peer;
const sip_reason_t *reason;
@@ -1297,20 +1305,15 @@ priv_nua_i_cancel_cb (TpsipMediaChannel *self,
if (message == NULL || !g_utf8_validate (message, -1, NULL))
message = "";
- set = tp_intset_new ();
- tp_intset_add (set, peer);
- tp_intset_add (set, mixin->self_handle);
+ remove = tp_intset_new ();
+ tp_intset_add (remove, peer);
+ tp_intset_add (remove, mixin->self_handle);
- tp_group_mixin_change_members ((GObject *) self,
- message,
- NULL, /* add */
- set, /* remove */
- NULL,
- NULL,
- actor,
- TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+ tp_group_mixin_change_members ((GObject *) self, message,
+ NULL, remove, NULL, NULL,
+ actor, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
- tp_intset_destroy (set);
+ tp_intset_destroy (remove);
return TRUE;
}
@@ -1391,7 +1394,22 @@ priv_nua_i_state_cb (TpsipMediaChannel *self,
TPSIP_CHANNEL_CALL_STATE_PROCEEDING_MASK);
if (status < 300)
- tpsip_media_session_accept (priv->session);
+ {
+ TpIntSet *add = tp_intset_new_containing (peer);
+
+ tp_group_mixin_change_members ((GObject *) self,
+ "",
+ add, /* add */
+ NULL, /* remove */
+ NULL,
+ NULL,
+ peer,
+ TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+
+ tp_intset_destroy (add);
+
+ tpsip_media_session_accept (priv->session);
+ }
else if (status == 491)
tpsip_media_session_resolve_glare (priv->session);
else
@@ -1445,12 +1463,10 @@ static void priv_session_state_changed_cb (TpsipMediaSession *session,
switch (state)
{
case TPSIP_MEDIA_SESSION_STATE_INVITE_SENT:
- set = tp_intset_new ();
-
g_assert (priv->initiator == self_handle);
/* add the peer to remote pending */
- tp_intset_add (set, peer);
+ set = tp_intset_new_containing (peer);
tp_group_mixin_change_members ((GObject *)channel,
"",
NULL, /* add */
@@ -1460,18 +1476,15 @@ static void priv_session_state_changed_cb (TpsipMediaSession *session,
self_handle, /* actor */
TP_CHANNEL_GROUP_CHANGE_REASON_INVITED);
- /* update flags: allow removal and rescinding, no more adding */
- tp_group_mixin_change_flags ((GObject *)channel,
- TP_CHANNEL_GROUP_FLAG_CAN_REMOVE | TP_CHANNEL_GROUP_FLAG_CAN_RESCIND,
+ /* update flags: no more adding */
+ tp_group_mixin_change_flags ((GObject *)channel, 0,
TP_CHANNEL_GROUP_FLAG_CAN_ADD);
break;
case TPSIP_MEDIA_SESSION_STATE_INVITE_RECEIVED:
- set = tp_intset_new ();
-
/* add ourself to local pending */
- tp_intset_add (set, self_handle);
+ set = tp_intset_new_containing (self_handle);
tp_group_mixin_change_members ((GObject *) channel, "",
NULL, /* add */
NULL, /* remove */
@@ -1480,10 +1493,15 @@ static void priv_session_state_changed_cb (TpsipMediaSession *session,
priv->initiator, /* actor */
TP_CHANNEL_GROUP_CHANGE_REASON_INVITED);
- /* No adding more members to the incoming call, removing is OK */
- tp_group_mixin_change_flags ((GObject *) channel,
- TP_CHANNEL_GROUP_FLAG_CAN_REMOVE,
- TP_CHANNEL_GROUP_FLAG_CAN_ADD);
+ /* No adding more members to the incoming call. Therefore also not
+ * possible to add anyone to remote-pending, so rescinding would make
+ * utterly no sense. We also disallow removing the remote peer if
+ * we are not the initiator, so disallow that too.
+ * Removing yourself to end the call is not represented by group flags.
+ */
+ tp_group_mixin_change_flags ((GObject *) channel, 0,
+ TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_REMOVE |
+ TP_CHANNEL_GROUP_FLAG_CAN_RESCIND);
break;
@@ -1493,10 +1511,8 @@ static void priv_session_state_changed_cb (TpsipMediaSession *session,
if (!tp_handle_set_is_member (mixin->remote_pending, peer))
break; /* no-op */
- set = tp_intset_new ();
-
/* the peer has promoted itself to members */
- tp_intset_add (set, peer);
+ set = tp_intset_new_containing (peer);
tp_group_mixin_change_members ((GObject *)channel, "",
set, /* add */
NULL, /* remove */
@@ -1509,10 +1525,8 @@ static void priv_session_state_changed_cb (TpsipMediaSession *session,
if (!tp_handle_set_is_member (mixin->local_pending, self_handle))
break; /* no-op */
- set = tp_intset_new ();
-
/* promote ourselves to members */
- tp_intset_add (set, self_handle);
+ set = tp_intset_new_containing (self_handle);
tp_group_mixin_change_members ((GObject *)channel, "",
set, /* add */
NULL, /* remove */
@@ -1521,11 +1535,12 @@ static void priv_session_state_changed_cb (TpsipMediaSession *session,
self_handle, 0);
}
- /* update flags: allow removal, deny adding and rescinding */
- tp_group_mixin_change_flags ((GObject *)channel,
- TP_CHANNEL_GROUP_FLAG_CAN_REMOVE,
- TP_CHANNEL_GROUP_FLAG_CAN_ADD
- | TP_CHANNEL_GROUP_FLAG_CAN_RESCIND);
+ /* update flags: deny adding and rescinding. Removing the remote peer is
+ * still allowed.
+ * Removing yourself to end the call is not represented by group flags.
+ */
+ tp_group_mixin_change_flags ((GObject *)channel, 0,
+ TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_RESCIND);
break;
@@ -1717,7 +1732,7 @@ _tpsip_media_channel_add_member (GObject *iface,
if (priv->initiator == mixin->self_handle)
{
- TpIntSet *set;
+ TpIntSet *remote_pending;
/* case a: an old-school outbound call
* (we are the initiator, a new handle added with AddMembers) */
@@ -1727,21 +1742,20 @@ _tpsip_media_channel_add_member (GObject *iface,
/* Backwards compatible behavior:
* add the peer to remote pending without waiting for the actual request
* to be sent */
- set = tp_intset_new ();
- tp_intset_add (set, handle);
+ remote_pending = tp_intset_new_containing (handle);
tp_group_mixin_change_members (iface,
"",
NULL, /* add */
NULL, /* remove */
NULL, /* local pending */
- set, /* remote pending */
+ remote_pending, /* remote pending */
mixin->self_handle, /* actor */
TP_CHANNEL_GROUP_CHANGE_REASON_INVITED);
- tp_intset_destroy (set);
+ tp_intset_destroy (remote_pending);
- /* update flags: allow removal and rescinding, no more adding */
- tp_group_mixin_change_flags (iface,
- TP_CHANNEL_GROUP_FLAG_CAN_REMOVE | TP_CHANNEL_GROUP_FLAG_CAN_RESCIND,
+ /* update flags: no more adding.
+ * Removal and rescinding are still allowed. */
+ tp_group_mixin_change_flags (iface, 0,
TP_CHANNEL_GROUP_FLAG_CAN_ADD);
return TRUE;
diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am
index 086e437..14bdc06 100644
--- a/tests/twisted/Makefile.am
+++ b/tests/twisted/Makefile.am
@@ -1,3 +1,5 @@
+NULL =
+
TWISTED_TESTS = \
cm/protocol.py \
test-register.py \
@@ -6,7 +8,10 @@ TWISTED_TESTS = \
test-handle-normalisation.py \
test-message.py \
test-self-alias.py \
- text/initiate-requestotron.py
+ text/initiate-requestotron.py \
+ voip/incoming-basics.py \
+ voip/outgoing-basics.py \
+ $(NULL)
TESTS =
diff --git a/tests/twisted/sofiatest.py b/tests/twisted/sofiatest.py
index 94263b9..23142b9 100644
--- a/tests/twisted/sofiatest.py
+++ b/tests/twisted/sofiatest.py
@@ -9,6 +9,8 @@ from twisted.internet import reactor
import os
import sys
+import random
+
import dbus
import dbus.glib
@@ -31,22 +33,31 @@ class SipProxy(sip.RegisterProxy):
def handle_request(self, message, addr):
if message.method == 'REGISTER':
return sip.RegisterProxy.handle_request(self, message, addr)
- if message.method == 'MESSAGE':
- self.event_func(servicetest.Event('sip-message',
+ elif message.method == 'OPTIONS' and \
+ 'REGISTRATION PROBE' == message.headers.get('subject','')[0]:
+ self.deliverResponse(self.responseFromRequest(200, message))
+ else:
+ headers = {}
+ for key, values in message.headers.items():
+ headers[key.replace('-', '_')] = values[0]
+ self.event_func(servicetest.Event('sip-%s' % message.method.lower(),
uri=str(message.uri), headers=message.headers, body=message.body,
- sip_message=message))
+ sip_message=message, **headers))
def handle_response(self, message, addr):
+ headers = {}
+ for key, values in message.headers.items():
+ headers[key.replace('-', '_')] = values[0]
self.event_func(servicetest.Event('sip-response',
code=message.code, headers=message.headers, body=message.body,
- sip_message=message))
+ sip_message=message, **headers))
def prepare_test(event_func, register_cb, params=None):
actual_params = {
'account': 'testacc@127.0.0.1',
'password': 'testpwd',
'proxy-host': '127.0.0.1',
- 'port': dbus.UInt16(9090),
+ 'port': dbus.UInt16(random.randint(9090, 9999)),
'local-ip-address': '127.0.0.1'
}
@@ -114,8 +125,8 @@ def exec_test(fun, params=None, register_cb=default_register_cb, timeout=None):
return '\x1b[32m%s\x1b[0m' % s
patterns = {
- 'handled': green,
- 'not handled': red,
+ 'handled,': green,
+ 'not hand': red,
}
class Colourer:
@@ -124,8 +135,11 @@ def exec_test(fun, params=None, register_cb=default_register_cb, timeout=None):
self.patterns = patterns
def write(self, s):
- f = self.patterns.get(s, lambda x: x)
+ f = self.patterns.get(s[:len('handled,')], lambda x: x)
self.fh.write(f(s))
+
+ def isatty(self):
+ return False
sys.stdout = Colourer(sys.stdout, patterns)
diff --git a/tests/twisted/voip/incoming-basics.py b/tests/twisted/voip/incoming-basics.py
new file mode 100644
index 0000000..c3cf8dd
--- /dev/null
+++ b/tests/twisted/voip/incoming-basics.py
@@ -0,0 +1,161 @@
+"""
+Test incoming call handling.
+"""
+
+import dbus
+from twisted.words.xish import xpath
+
+from sofiatest import exec_test
+from servicetest import (
+ make_channel_proxy, wrap_channel,
+ EventPattern, call_async,
+ assertEquals, assertContains, assertLength,
+ )
+import constants as cs
+from voip_test import VoipTestContext
+
+from twisted.words.xish import xpath
+
+def test(q, bus, conn, sip_proxy, peer='foo@bar.com'):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged', args=[0, 1])
+
+ context = VoipTestContext(q, conn, bus, sip_proxy, 'sip:testacc@127.0.0.1', peer)
+
+ self_handle = conn.GetSelfHandle()
+ remote_handle = conn.RequestHandles(cs.HT_CONTACT, [context.peer])[0]
+
+ # Remote end calls us
+ context.incoming_call()
+
+ nc, e = q.expect_many(
+ EventPattern('dbus-signal', signal='NewChannels'),
+ EventPattern('dbus-signal', signal='NewSessionHandler'),
+ )[0:2]
+
+ path, props = nc.args[0][0]
+ ct = props[cs.CHANNEL_TYPE]
+ ht = props[cs.CHANNEL + '.TargetHandleType']
+ h = props[cs.CHANNEL + '.TargetHandle']
+
+ assert ct == cs.CHANNEL_TYPE_STREAMED_MEDIA, ct
+ assert ht == cs.HT_CONTACT, ht
+ assert h == remote_handle, h
+
+ media_chan = make_channel_proxy(conn, path, 'Channel.Interface.Group')
+ media_iface = make_channel_proxy(conn, path, 'Channel.Type.StreamedMedia')
+
+ # S-E was notified about new session handler, and calls Ready on it
+ assert e.args[1] == 'rtp'
+ session_handler = make_channel_proxy(conn, e.args[0], 'Media.SessionHandler')
+ session_handler.Ready()
+
+ nsh_event = q.expect('dbus-signal', signal='NewStreamHandler')
+
+ # S-E gets notified about a newly-created stream
+ stream_handler = make_channel_proxy(conn, nsh_event.args[0],
+ 'Media.StreamHandler')
+
+ streams = media_iface.ListStreams()
+ assertLength(1, streams)
+
+ stream_id, stream_handle, stream_type, _, stream_direction, pending_flags =\
+ streams[0]
+ assertEquals(remote_handle, stream_handle)
+ assertEquals(cs.MEDIA_STREAM_TYPE_AUDIO, stream_type)
+ assertEquals(cs.MEDIA_STREAM_DIRECTION_RECEIVE, stream_direction)
+ assertEquals(cs.MEDIA_STREAM_PENDING_LOCAL_SEND, pending_flags)
+
+ # Exercise channel properties
+ channel_props = media_chan.GetAll(
+ cs.CHANNEL, dbus_interface=dbus.PROPERTIES_IFACE)
+ assertEquals(remote_handle, channel_props['TargetHandle'])
+ assertEquals(cs.HT_CONTACT, channel_props['TargetHandleType'])
+ assertEquals((cs.HT_CONTACT, remote_handle),
+ media_chan.GetHandle(dbus_interface=cs.CHANNEL))
+ assertEquals(context.peer_id, channel_props['TargetID'])
+ assertEquals(context.peer_id, channel_props['InitiatorID'])
+ assertEquals(remote_handle, channel_props['InitiatorHandle'])
+ assertEquals(False, channel_props['Requested'])
+
+ group_props = media_chan.GetAll(
+ cs.CHANNEL_IFACE_GROUP, dbus_interface=dbus.PROPERTIES_IFACE)
+
+ assert group_props['SelfHandle'] == self_handle, \
+ (group_props['SelfHandle'], self_handle)
+
+ flags = group_props['GroupFlags']
+ assert flags & cs.GF_PROPERTIES, flags
+ # Changing members in any way other than adding or removing yourself is
+ # meaningless for incoming calls, and the flags need not be sent to change
+ # your own membership.
+ assert not flags & cs.GF_CAN_ADD, flags
+ assert not flags & cs.GF_CAN_REMOVE, flags
+ assert not flags & cs.GF_CAN_RESCIND, flags
+
+ assert group_props['Members'] == [remote_handle], group_props['Members']
+ assert group_props['RemotePendingMembers'] == [], \
+ group_props['RemotePendingMembers']
+ # We're local pending because remote_handle invited us.
+ assert group_props['LocalPendingMembers'] == \
+ [(self_handle, remote_handle, cs.GC_REASON_INVITED, '')], \
+ unwrap(group_props['LocalPendingMembers'])
+
+ streams = media_chan.ListStreams(
+ dbus_interface=cs.CHANNEL_TYPE_STREAMED_MEDIA)
+ assert len(streams) == 1, streams
+ assert len(streams[0]) == 6, streams[0]
+ # streams[0][0] is the stream identifier, which in principle we can't
+ # make any assertion about (although in practice it's probably 1)
+ assert streams[0][1] == remote_handle, (streams[0], remote_handle)
+ assert streams[0][2] == cs.MEDIA_STREAM_TYPE_AUDIO, streams[0]
+ # We haven't connected yet
+ assert streams[0][3] == cs.MEDIA_STREAM_STATE_DISCONNECTED, streams[0]
+ # In Gabble, incoming streams start off with remote send enabled, and
+ # local send requested
+ assert streams[0][4] == cs.MEDIA_STREAM_DIRECTION_RECEIVE, streams[0]
+ assert streams[0][5] == cs.MEDIA_STREAM_PENDING_LOCAL_SEND, streams[0]
+
+ # Connectivity checks happen before we have accepted the call
+ stream_handler.NewNativeCandidate("fake", context.get_remote_transports_dbus())
+ stream_handler.NativeCandidatesPrepared()
+ stream_handler.Ready(context.get_audio_codecs_dbus())
+ stream_handler.StreamState(cs.MEDIA_STREAM_STATE_CONNECTED)
+ stream_handler.SupportedCodecs(context.get_audio_codecs_dbus())
+
+ # At last, accept the call
+ media_chan.AddMembers([self_handle], 'accepted')
+
+ # Call is accepted, we become a member, and the stream that was pending
+ # local send is now sending.
+ memb, acc, _, _, _ = q.expect_many(
+ EventPattern('dbus-signal', signal='MembersChanged',
+ args=[u'', [self_handle], [], [], [], self_handle,
+ cs.GC_REASON_NONE]),
+ EventPattern('sip-response', call_id=context.call_id, code=200),
+ EventPattern('dbus-signal', signal='SetStreamSending', args=[True]),
+ EventPattern('dbus-signal', signal='SetStreamPlaying', args=[True]),
+ EventPattern('dbus-signal', signal='StreamDirectionChanged',
+ args=[stream_id, cs.MEDIA_STREAM_DIRECTION_BIDIRECTIONAL, 0]),
+ )
+
+ context.check_call_sdp(acc.sip_message.body)
+
+ context.ack(acc.sip_message)
+
+ # we are now both in members
+ members = media_chan.GetMembers()
+ assert set(members) == set([self_handle, remote_handle]), members
+
+ # Connected! Blah, blah, ...
+
+ # 'Nuff said
+ bye_msg = context.terminate()
+
+ q.expect_many(EventPattern('dbus-signal', signal='Closed', path=path),
+ EventPattern('sip-response', cseq=bye_msg.headers['cseq'][0]))
+
+if __name__ == '__main__':
+ exec_test(test)
+ exec_test(lambda q, bus, conn, stream:
+ test(q, bus, conn, stream, 'foo@sip.bar.com'))
diff --git a/tests/twisted/voip/outgoing-basics.py b/tests/twisted/voip/outgoing-basics.py
new file mode 100644
index 0000000..933b4ad
--- /dev/null
+++ b/tests/twisted/voip/outgoing-basics.py
@@ -0,0 +1,297 @@
+"""
+Test basic outgoing call handling, using CreateChannel and all three variations
+of RequestChannel.
+"""
+
+import dbus
+from twisted.words.xish import xpath
+
+from sofiatest import exec_test
+from servicetest import (
+ make_channel_proxy, wrap_channel,
+ EventPattern, call_async,
+ assertEquals, assertContains, assertLength,
+ )
+import constants as cs
+from voip_test import VoipTestContext
+
+# There are various deprecated APIs for requesting calls, documented at
+# <http://telepathy.freedesktop.org/wiki/Requesting StreamedMedia channels>.
+# These are ordered from most recent to most deprecated.
+CREATE = 0 # CreateChannel({TargetHandleType: Contact, TargetHandle: h});
+ # RequestStreams()
+REQUEST_ANONYMOUS = 1 # RequestChannel(HandleTypeNone, 0); RequestStreams()
+REQUEST_ANONYMOUS_AND_ADD = 2 # RequestChannel(HandleTypeNone, 0);
+ # AddMembers([h], ...); RequestStreams(h,...)
+REQUEST_NONYMOUS = 3 # RequestChannel(HandleTypeContact, h);
+ # RequestStreams(h, ...)
+
+def create(q, bus, conn, sip_proxy, peer='foo@bar.com'):
+ worker(q, bus, conn, sip_proxy, CREATE, peer)
+
+def request_anonymous(q, bus, conn, sip_proxy, peer='publish@foo.com'):
+ worker(q, bus, conn, sip_proxy, REQUEST_ANONYMOUS, peer)
+
+def request_anonymous_and_add(q, bus, conn, sip_proxy,
+ peer='publish-subscribe@foo.com/Res'):
+ worker(q, bus, conn, sip_proxy, REQUEST_ANONYMOUS_AND_ADD, peer)
+
+def request_nonymous(q, bus, conn, sip_proxy, peer='subscribe@foo.com'):
+ worker(q, bus, conn, sip_proxy, REQUEST_NONYMOUS, peer)
+
+def worker(q, bus, conn, sip_proxy, variant, peer):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged', args=[0, 1])
+
+ self_handle = conn.GetSelfHandle()
+ context = VoipTestContext(q, conn, bus, sip_proxy, 'sip:testacc@127.0.0.1', peer)
+
+ self_handle = conn.GetSelfHandle()
+ remote_handle = conn.RequestHandles(1, [context.peer])[0]
+
+ if variant == REQUEST_NONYMOUS:
+ path = conn.RequestChannel(cs.CHANNEL_TYPE_STREAMED_MEDIA,
+ cs.HT_CONTACT, remote_handle, True)
+ elif variant == CREATE:
+ path = conn.Requests.CreateChannel({
+ cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_STREAMED_MEDIA,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_HANDLE: remote_handle,
+ })[0]
+ else:
+ path = conn.RequestChannel(cs.CHANNEL_TYPE_STREAMED_MEDIA,
+ cs.HT_NONE, 0, True)
+
+ old_sig, new_sig = q.expect_many(
+ EventPattern('dbus-signal', signal='NewChannel',
+ predicate=lambda e: cs.CHANNEL_TYPE_CONTACT_LIST not in e.args),
+ EventPattern('dbus-signal', signal='NewChannels',
+ predicate=lambda e:
+ cs.CHANNEL_TYPE_CONTACT_LIST not in e.args[0][0][1].values()),
+ )
+
+ if variant == REQUEST_NONYMOUS or variant == CREATE:
+ assertEquals( [path, cs.CHANNEL_TYPE_STREAMED_MEDIA, cs.HT_CONTACT,
+ remote_handle, True], old_sig.args)
+ else:
+ assertEquals( [path, cs.CHANNEL_TYPE_STREAMED_MEDIA, cs.HT_NONE, 0,
+ True], old_sig.args)
+
+ assertLength(1, new_sig.args)
+ assertLength(1, new_sig.args[0]) # one channel
+ assertLength(2, new_sig.args[0][0]) # two struct members
+ emitted_props = new_sig.args[0][0][1]
+
+ assertEquals(
+ cs.CHANNEL_TYPE_STREAMED_MEDIA, emitted_props[cs.CHANNEL_TYPE])
+
+ if variant == REQUEST_NONYMOUS or variant == CREATE:
+ assertEquals(remote_handle, emitted_props[cs.TARGET_HANDLE])
+ assertEquals(cs.HT_CONTACT, emitted_props[cs.TARGET_HANDLE_TYPE])
+ assertEquals(context.peer_id, emitted_props[cs.TARGET_ID])
+ else:
+ assertEquals(0, emitted_props[cs.TARGET_HANDLE])
+ assertEquals(cs.HT_NONE, emitted_props[cs.TARGET_HANDLE_TYPE])
+ assertEquals('', emitted_props[cs.TARGET_ID])
+
+ assertEquals(True, emitted_props[cs.REQUESTED])
+ assertEquals(self_handle, emitted_props[cs.INITIATOR_HANDLE])
+ assertEquals('sip:testacc@127.0.0.1', emitted_props[cs.INITIATOR_ID])
+
+ chan = wrap_channel(bus.get_object(conn.bus_name, path), 'StreamedMedia',
+ ['MediaSignalling'])
+
+ # Exercise basic Channel Properties
+ channel_props = chan.Properties.GetAll(cs.CHANNEL)
+
+ assertEquals(cs.CHANNEL_TYPE_STREAMED_MEDIA,
+ channel_props.get('ChannelType'))
+
+ if variant == REQUEST_NONYMOUS or variant == CREATE:
+ assertEquals(remote_handle, channel_props['TargetHandle'])
+ assertEquals(cs.HT_CONTACT, channel_props['TargetHandleType'])
+ assertEquals(context.peer_id, channel_props['TargetID'])
+ assertEquals((cs.HT_CONTACT, remote_handle), chan.GetHandle())
+ else:
+ assertEquals(0, channel_props['TargetHandle'])
+ assertEquals(cs.HT_NONE, channel_props['TargetHandleType'])
+ assertEquals('', channel_props['TargetID'])
+ assertEquals((cs.HT_NONE, 0), chan.GetHandle())
+
+ for interface in [
+ cs.CHANNEL_IFACE_GROUP, cs.CHANNEL_IFACE_MEDIA_SIGNALLING,
+ cs.TP_AWKWARD_PROPERTIES, cs.CHANNEL_IFACE_HOLD]:
+ assertContains(interface, channel_props['Interfaces'])
+
+ assertEquals(True, channel_props['Requested'])
+ assertEquals('sip:testacc@127.0.0.1', channel_props['InitiatorID'])
+ assertEquals(conn.GetSelfHandle(), channel_props['InitiatorHandle'])
+
+ # Exercise Group Properties
+ group_props = chan.Properties.GetAll(cs.CHANNEL_IFACE_GROUP)
+
+ assertEquals([self_handle], group_props['Members'])
+ assertEquals([], group_props['LocalPendingMembers'])
+
+ if variant == REQUEST_NONYMOUS:
+ # In this variant, they're meant to be in RP even though we've sent
+ # nothing
+ assertEquals([remote_handle], group_props['RemotePendingMembers'])
+ else:
+ # For an anonymous channel, the peer isn't yet known; for a Create-d
+ # channel, the peer only appears in RP when we actually send them the
+ # session-initiate
+ assertEquals([], group_props['RemotePendingMembers'])
+
+ if variant == REQUEST_ANONYMOUS_AND_ADD:
+ # but we should be allowed to add the peer.
+ chan.Group.AddMembers([remote_handle], 'I love backwards compat')
+
+ base_flags = cs.GF_PROPERTIES | cs.GF_CAN_REMOVE | cs.GF_CAN_RESCIND
+
+ if variant in [REQUEST_ANONYMOUS_AND_ADD, REQUEST_ANONYMOUS, CREATE]:
+ expected_flags = base_flags | cs.GF_CAN_ADD
+ else:
+ expected_flags = base_flags
+
+ assertEquals(bin(expected_flags), bin(group_props['GroupFlags']))
+ assertEquals({}, group_props['HandleOwners'])
+
+ assertEquals([], chan.StreamedMedia.ListStreams())
+ streams = chan.StreamedMedia.RequestStreams(remote_handle,
+ [cs.MEDIA_STREAM_TYPE_AUDIO])
+ assertEquals(streams, chan.StreamedMedia.ListStreams())
+ assertLength(1, streams)
+
+ # streams[0][0] is the stream identifier, which in principle we can't
+ # make any assertion about (although in practice it's probably 1)
+
+ assertEquals((
+ remote_handle,
+ cs.MEDIA_STREAM_TYPE_AUDIO,
+ # We haven't connected yet
+ cs.MEDIA_STREAM_STATE_DISCONNECTED,
+ # In Gabble, requested streams start off bidirectional
+ cs.MEDIA_STREAM_DIRECTION_BIDIRECTIONAL,
+ cs.MEDIA_STREAM_PENDING_REMOTE_SEND),
+ streams[0][1:])
+
+ # S-E does state recovery to get the session handler, and calls Ready on it
+ session_handlers = chan.MediaSignalling.GetSessionHandlers()
+ sh_path, sh_type = session_handlers[0]
+
+ assert sh_type == 'rtp'
+
+ session_handler = make_channel_proxy(conn, sh_path, 'Media.SessionHandler')
+ session_handler.Ready()
+
+ e = q.expect('dbus-signal', signal='NewStreamHandler')
+
+ stream_handler = make_channel_proxy(conn, e.args[0], 'Media.StreamHandler')
+
+ stream_handler.NewNativeCandidate("fake", context.get_remote_transports_dbus())
+ stream_handler.NativeCandidatesPrepared()
+ stream_handler.Ready(context.get_audio_codecs_dbus())
+ stream_handler.StreamState(cs.MEDIA_STREAM_STATE_CONNECTED)
+
+ sh_props = stream_handler.GetAll(
+ cs.STREAM_HANDLER, dbus_interface=dbus.PROPERTIES_IFACE)
+ assertEquals('none', sh_props['NATTraversal'])
+ assertEquals(True, sh_props['CreatedLocally'])
+
+ if variant == CREATE:
+ # When we actually send INVITE to the peer, they should pop up in remote
+ # pending.
+ invite_event, _ = q.expect_many(
+ EventPattern('sip-invite'),
+ EventPattern('dbus-signal', signal='MembersChanged',
+ args=["", [], [], [], [remote_handle], self_handle,
+ cs.GC_REASON_INVITED]),
+ )
+ else:
+ invite_event = q.expect('sip-invite')
+
+ # Check the Group interface's properties again. Regardless of the call
+ # requesting API in use, the state should be the same here:
+ group_props = chan.Properties.GetAll(cs.CHANNEL_IFACE_GROUP)
+ assertContains('HandleOwners', group_props)
+ assertEquals([self_handle], group_props['Members'])
+ assertEquals([], group_props['LocalPendingMembers'])
+ assertEquals([remote_handle], group_props['RemotePendingMembers'])
+
+ context.check_call_sdp(invite_event.sip_message.body)
+ context.accept(invite_event.sip_message)
+
+ ack_cseq = "%s ACK" % invite_event.cseq.split()[0]
+ q.expect_many(
+ EventPattern('sip-ack', cseq=ack_cseq),
+ # Call accepted
+ EventPattern('dbus-signal', signal='MembersChanged',
+ args=['', [remote_handle], [], [], [], remote_handle,
+ cs.GC_REASON_NONE]),
+ )
+
+ # Time passes ... afterwards we close the chan
+
+ chan.Group.RemoveMembers([self_handle], 'closed')
+
+
+ mc_event, _, bye_event = q.expect_many(
+ EventPattern('dbus-signal', signal='MembersChanged'),
+ EventPattern('dbus-signal', signal='Close'),
+ EventPattern('sip-bye', call_id=context.call_id),
+ )
+ # Check that we're the actor
+ assertEquals(self_handle, mc_event.args[5])
+
+ # For completeness, reply to the BYE.
+ bye_response = sip_proxy.responseFromRequest(200, bye_event.sip_message)
+ sip_proxy.deliverResponse(bye_response)
+
+def rccs(q, bus, conn, stream):
+ """
+ Tests that the connection's RequestableChannelClasses for StreamedMedia are
+ sane.
+ """
+ conn.Connect()
+
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ rccs = conn.Properties.Get(cs.CONN_IFACE_REQUESTS,
+ 'RequestableChannelClasses')
+
+ # Test Channel.Type.StreamedMedia
+ media_classes = [ rcc for rcc in rccs
+ if rcc[0][cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_STREAMED_MEDIA ]
+
+ assertLength(1, media_classes)
+
+ fixed, allowed = media_classes[0]
+
+ assertEquals(cs.HT_CONTACT, fixed[cs.TARGET_HANDLE_TYPE])
+
+ expected_allowed = [
+ cs.TARGET_ID, cs.TARGET_HANDLE,
+ cs.INITIAL_VIDEO, cs.INITIAL_AUDIO
+ ]
+
+ allowed.sort()
+ expected_allowed.sort()
+ assertEquals(expected_allowed, allowed)
+
+if __name__ == '__main__':
+
+ exec_test(rccs)
+ exec_test(create)
+ exec_test(request_anonymous)
+ exec_test(request_anonymous_and_add)
+ exec_test(request_nonymous)
+ exec_test(lambda q, b, c, s:
+ create(q, b, c, s, peer='foo@gw.bar.com'))
+ exec_test(lambda q, b, c, s:
+ request_anonymous(q, b, c, s, peer='foo@gw.bar.com'))
+ exec_test(lambda q, b, c, s:
+ request_anonymous_and_add(q, b, c, s, peer='foo@gw.bar.com'))
+ exec_test(lambda q, b, c, s:
+ request_nonymous(q, b, c, s, peer='foo@gw.bar.com'))
diff --git a/tests/twisted/voip/voip_test.py b/tests/twisted/voip/voip_test.py
new file mode 100644
index 0000000..0b41828
--- /dev/null
+++ b/tests/twisted/voip/voip_test.py
@@ -0,0 +1,172 @@
+
+import dbus
+import uuid
+
+import twisted.protocols.sip
+
+from servicetest import assertContains
+
+class VoipTestContext(object):
+ # Default audio codecs for the remote end
+ audio_codecs = [ ('GSM', 3, 8000, {}),
+ ('PCMA', 8, 8000, {}),
+ ('PCMU', 0, 8000, {}) ]
+
+ # Default video codecs for the remote end. I have no idea what's
+ # a suitable value here...
+ video_codecs = [ ('WTF', 42, 80000, {}) ]
+
+ # Default candidates for the remote end
+ remote_transports = [
+ ( "192.168.0.1", # host
+ 666, # port
+ 0, # protocol = TP_MEDIA_STREAM_BASE_PROTO_UDP
+ "RTP", # protocol subtype
+ "AVP", # profile
+ 1.0, # preference
+ 0, # transport type = TP_MEDIA_STREAM_TRANSPORT_TYPE_LOCAL,
+ "username",
+ "password" ) ]
+
+ _mline_template = 'm=audio %(port)s %(subtype)s/%(profile)s %(codec_ids)s'
+ _aline_template = 'a=rtpmap:%(codec_id)s %(name)s/%(rate)s'
+
+ def __init__(self, q, conn, bus, sip_proxy, our_uri, peer):
+ self.bus = bus
+ self.conn = conn
+ self.q = q
+ self.our_uri = our_uri
+ self.peer = peer
+ self.peer_id = "sip:" + peer
+ self.sip_proxy = sip_proxy
+ self._cseq_id = 1
+
+ def dbusify_codecs(self, codecs):
+ dbussed_codecs = [ (id, name, 0, rate, 0, params )
+ for (name, id, rate, params) in codecs ]
+ return dbus.Array(dbussed_codecs, signature='(usuuua{ss})')
+
+ def dbusify_codecs_with_params (self, codecs):
+ return self.dbusify_codecs(codecs)
+
+ def get_audio_codecs_dbus(self):
+ return self.dbusify_codecs(self.audio_codecs)
+
+ def get_video_codecs_dbus(self):
+ return self.dbusify_codecs(self.video_codecs)
+
+ def dbusify_call_codecs(self, codecs):
+ dbussed_codecs = [ (id, name, rate, 0, params)
+ for (name, id, rate, params) in codecs ]
+ return dbus.Array(dbussed_codecs, signature='(usuua{ss})')
+
+ def dbusify_call_codecs_with_params(self, codecs):
+ return dbusify_call_codecs (self, codecs)
+
+ def get_call_audio_codecs_dbus(self):
+ return self.dbusify_call_codecs(self.audio_codecs)
+
+ def get_call_video_codecs_dbus(self):
+ return self.dbusify_call_codecs(self.video_codecs)
+
+
+ def get_remote_transports_dbus(self):
+ return dbus.Array([
+ (dbus.UInt32(1 + i), host, port, proto, subtype,
+ profile, pref, transtype, user, pwd)
+ for i, (host, port, proto, subtype, profile,
+ pref, transtype, user, pwd)
+ in enumerate(self.remote_transports) ],
+ signature='(usuussduss)')
+
+ def get_call_remote_transports_dbus(self):
+ return dbus.Array([
+ (1 , host, port,
+ { "Type": transtype,
+ "Foundation": "",
+ "Protocol": proto,
+ "Priority": int((1+i) * 65536),
+ "Username": user,
+ "Password": pwd }
+ ) for i, (host, port, proto, subtype, profile,
+ pref, transtype, user, pwd)
+ in enumerate(self.remote_transports) ],
+ signature='(usqa{sv})')
+
+ def get_call_sdp(self):
+ (ip, port, protocol, subtype, profile, preference,
+ transport, username, password) = self.remote_transports[0]
+
+ codec_id_list = []
+ codec_list = []
+ for name, codec_id, rate, _misc in self.audio_codecs:
+ codec_list.append('a=rtpmap:%(codec_id)s %(name)s/%(rate)s' % locals())
+ codec_id_list.append(str(codec_id))
+ codec_ids = ' '.join(codec_id_list)
+ codecs = '\r\n'.join(codec_list)
+
+ sdp_string = ('v=0\r\n'
+ 'o=- 7047265765596858314 2813734028456100815 IN IP4 %(ip)s\r\n'
+ 's=-\r\n'
+ 't=0 0\r\n'
+ 'm=audio %(port)s RTP/AVP 3 8 0\r\n'
+ 'c=IN IP4 %(ip)s\r\n'
+ '%(codecs)s\r\n') % locals()
+ return sdp_string
+
+ def check_call_sdp(self, sdp_string):
+ codec_id_list = []
+ for name, codec_id, rate, _misc in self.audio_codecs:
+ assertContains (self._aline_template % locals(), sdp_string)
+ codec_id_list.append(str(codec_id))
+ codec_ids = ' '.join(codec_id_list)
+
+ (ip, port, protocol, subtype, profile, preference,
+ transport, username, password) = self.remote_transports[0]
+ assert self._mline_template % locals() in sdp_string
+
+ def send_message(self, message_type, body='', **additional_headers):
+ url = twisted.protocols.sip.parseURL('sip:testacc@127.0.0.1')
+ msg = twisted.protocols.sip.Request(message_type, url)
+ if body:
+ msg.body = body
+ msg.addHeader('content-length', '%d' % len(msg.body))
+ msg.addHeader('from', '<%s>;tag=XYZ' % self.peer_id)
+ msg.addHeader('to', '<sip:testacc@127.0.0.1>')
+ self._cseq_id += 1
+ additional_headers.setdefault('cseq', '%d %s' % (self._cseq_id, message_type))
+ for key, vals in additional_headers.items():
+ if not isinstance(vals, list):
+ vals = [vals]
+ k = key.replace('_', '-')
+ for v in vals:
+ msg.addHeader(k, v)
+ via = self.sip_proxy.getVia()
+ via.branch = 'z9hG4bKXYZ'
+ msg.addHeader('via', via.toString())
+ _expire, destination = self.sip_proxy.registry.users['testacc']
+ self.sip_proxy.sendMessage(destination, msg)
+ return msg
+
+ def accept(self, invite_message):
+ self.call_id = invite_message.headers['call-id'][0]
+ response = self.sip_proxy.responseFromRequest(200, invite_message)
+ # Echo sofiasip's SDP back to it. It doesn't care.
+ response.addHeader('content-type', 'application/sdp')
+ response.body = invite_message.body
+ response.addHeader('content-length', '%d' % len(response.body))
+ self.sip_proxy.deliverResponse(response)
+ return response
+
+ def ack(self, ok_message):
+ cseq = '%s ACK' % ok_message.headers['cseq'][0].split()[0]
+ self.send_message('ACK', call_id=self.call_id, cseq=cseq)
+
+ def incoming_call(self):
+ self.call_id = uuid.uuid4().hex
+ body = self.get_call_sdp()
+ return self.send_message('INVITE', body, content_type='application/sdp',
+ supported='timer, 100rel', call_id=self.call_id)
+
+ def terminate(self):
+ return self.send_message('BYE', call_id=self.call_id)