1"""
2Test everything related to contents
3"""
4
5from gabbletest import sync_stream
6from servicetest import (
7    make_channel_proxy, assertEquals, EventPattern)
8import constants as cs
9from jingletest2 import (
10    JingleTest2, JingleProtocol015, JingleProtocol031, test_dialects)
11
12from twisted.words.xish import xpath
13
14from config import VOIP_ENABLED
15
16if not VOIP_ENABLED:
17    print "NOTE: built with --disable-voip"
18    raise SystemExit(77)
19
20def worker(jp, q, bus, conn, stream):
21
22    def make_stream_request(stream_type):
23        media_iface.RequestStreams(remote_handle, [stream_type])
24
25        e = q.expect('dbus-signal', signal='NewStreamHandler')
26        stream_id = e.args[1]
27
28        stream_handler = make_channel_proxy(conn, e.args[0], 'Media.StreamHandler')
29
30        stream_handler.NewNativeCandidate("fake", jt2.get_remote_transports_dbus())
31        stream_handler.Ready(jt2.get_audio_codecs_dbus())
32        stream_handler.StreamState(cs.MEDIA_STREAM_STATE_CONNECTED)
33        return (stream_handler, stream_id)
34
35
36    jt2 = JingleTest2(jp, conn, q, stream, 'test@localhost', 'foo@bar.com/Foo')
37    jt2.prepare()
38
39    self_handle = conn.GetSelfHandle()
40    remote_handle = conn.RequestHandles(cs.HT_CONTACT, ["foo@bar.com/Foo"])[0]
41
42    # Remote end calls us
43    jt2.incoming_call()
44
45    # FIXME: these signals are not observable by real clients, since they
46    #        happen before NewChannels.
47    # The caller is in members
48    e = q.expect('dbus-signal', signal='MembersChanged',
49             args=[u'', [remote_handle], [], [], [], 0, 0])
50
51    # We're pending because of remote_handle
52    e = q.expect('dbus-signal', signal='MembersChanged',
53             args=[u'', [], [], [self_handle], [], remote_handle,
54                   cs.GC_REASON_INVITED])
55
56    media_chan = make_channel_proxy(conn, e.path, 'Channel.Interface.Group')
57    signalling_iface = make_channel_proxy(conn, e.path, 'Channel.Interface.MediaSignalling')
58    media_iface = make_channel_proxy(conn, e.path, 'Channel.Type.StreamedMedia')
59
60    # S-E gets notified about new session handler, and calls Ready on it
61    e = q.expect('dbus-signal', signal='NewSessionHandler')
62    assert e.args[1] == 'rtp'
63
64    session_handler = make_channel_proxy(conn, e.args[0], 'Media.SessionHandler')
65    session_handler.Ready()
66
67    media_chan.AddMembers([self_handle], 'accepted')
68
69    # S-E gets notified about a newly-created stream
70    e = q.expect('dbus-signal', signal='NewStreamHandler')
71    id1 = e.args[1]
72
73    stream_handler = make_channel_proxy(conn, e.args[0], 'Media.StreamHandler')
74
75    # We are now in members too
76    e = q.expect('dbus-signal', signal='MembersChanged',
77             args=[u'', [self_handle], [], [], [], self_handle,
78                   cs.GC_REASON_NONE])
79
80    # we are now both in members
81    members = media_chan.GetMembers()
82    assert set(members) == set([self_handle, remote_handle]), members
83
84    stream_handler.NewNativeCandidate("fake", jt2.get_remote_transports_dbus())
85    stream_handler.Ready(jt2.get_audio_codecs_dbus())
86    stream_handler.StreamState(cs.MEDIA_STREAM_STATE_CONNECTED)
87
88    # First one is transport-info
89    e = q.expect('stream-iq', predicate=jp.action_predicate('transport-info'))
90    assertEquals('foo@bar.com/Foo', e.query['initiator'])
91
92    # stream.send(gabbletest.make_result_iq(stream, e.stanza))
93    stream.send(jp.xml(jp.ResultIq('test@localhost', e.stanza, [])))
94
95    # S-E reports codec intersection, after which gabble can send acceptance
96    stream_handler.SupportedCodecs(jt2.get_audio_codecs_dbus())
97
98    # Second one is session-accept
99    e = q.expect('stream-iq', predicate=jp.action_predicate('session-accept'))
100
101    # stream.send(gabbletest.make_result_iq(stream, e.stanza))
102    stream.send(jp.xml(jp.ResultIq('test@localhost', e.stanza, [])))
103
104    # Here starts the interesting part of this test
105    # Remote end tries to create a content we can't handle
106    node = jp.SetIq(jt2.peer, jt2.jid, [
107        jp.Jingle(jt2.sid, jt2.peer, 'content-add', [
108            jp.Content('bogus', 'initiator', 'both',
109                jp.Description('hologram', [
110                    jp.PayloadType(name, str(rate), str(id), parameters) for
111                        (name, id, rate, parameters) in jt2.audio_codecs ]),
112            jp.TransportGoogleP2P()) ]) ])
113    stream.send(jp.xml(node))
114
115    # In older Jingle, this is a separate namespace, which isn't
116    # recognized, but it's a valid request, so it gets ackd and rejected
117    if jp.dialect == 'jingle-v0.15':
118        # Gabble should acknowledge content-add
119        q.expect('stream-iq', iq_type='result')
120
121        # .. and then send content-reject for the bogus content
122        e = q.expect('stream-iq', iq_type='set', predicate=lambda x:
123            xpath.queryForNodes("/iq/jingle[@action='content-reject']/content[@name='bogus']",
124                x.stanza))
125
126    # In new Jingle, this is a bogus subtype of recognized namespace,
127    # so Gabble returns a bad request error
128    else:
129        q.expect('stream-iq', iq_type='error')
130
131    # Remote end then tries to create a content with a name it's already used
132    node = jp.SetIq(jt2.peer, jt2.jid, [
133        jp.Jingle(jt2.sid, jt2.peer, 'content-add', [
134            jp.Content(jt2.audio_names[0], 'initiator', 'both',
135                jp.Description('audio', [
136                    jp.PayloadType(name, str(rate), str(id), parameters) for
137                        (name, id, rate, parameters) in jt2.audio_codecs ]),
138            jp.TransportGoogleP2P()) ]) ])
139    stream.send(jp.xml(node))
140
141    # Gabble should return error (content already exists)
142    q.expect('stream-iq', iq_type='error')
143
144    # We try to add a stream
145    (stream_handler2, id2) = make_stream_request(cs.MEDIA_STREAM_TYPE_VIDEO)
146
147    # Gabble should now send content-add
148    e = q.expect('stream-iq', iq_type='set', predicate=lambda x:
149        xpath.queryForNodes("/iq/jingle[@action='content-add']",
150            x.stanza))
151
152    c = e.query.firstChildElement()
153    assert c['creator'] == 'responder', c['creator']
154
155    stream.send(jp.xml(jp.ResultIq('test@localhost', e.stanza, [])))
156
157    # We try to add yet another stream
158    (stream_handler3, id3) = make_stream_request(cs.MEDIA_STREAM_TYPE_VIDEO)
159
160    # Gabble should send another content-add
161    e = q.expect('stream-iq', iq_type='set', predicate=lambda x:
162        xpath.queryForNodes("/iq/jingle[@action='content-add']",
163            x.stanza))
164
165    d = e.query.firstChildElement()
166    assertEquals('responder', d['creator'])
167
168    stream.send(jp.xml(jp.ResultIq('test@localhost', e.stanza, [])))
169
170    # Remote end rejects the first stream we tried to add.
171    node = jp.SetIq(jt2.peer, jt2.jid, [
172        jp.Jingle(jt2.sid, jt2.peer, 'content-reject', [
173            jp.Content(c['name'], c['creator'], c['senders']) ]) ])
174    stream.send(jp.xml(node))
175
176    # Gabble removes the stream
177    q.expect('dbus-signal', signal='StreamRemoved',
178        interface=cs.CHANNEL_TYPE_STREAMED_MEDIA)
179
180    # Remote end tries to add a content with the same name as the second one we
181    # just added
182    node = jp.SetIq(jt2.peer, jt2.jid, [
183        jp.Jingle(jt2.sid, jt2.peer, 'content-add', [
184            jp.Content(d['name'], 'initiator', 'both',
185                jp.Description('audio', [
186                    jp.PayloadType(name, str(rate), str(id), parameters) for
187                        (name, id, rate, parameters) in jt2.audio_codecs ]),
188            jp.TransportGoogleP2P()) ]) ])
189    stream.send(jp.xml(node))
190
191    # Because stream names are namespaced by creator, Gabble should be okay
192    # with that.
193    q.expect_many(
194        EventPattern('stream-iq', iq_type='result', iq_id=node[2]['id']),
195        EventPattern('dbus-signal', signal='StreamAdded'),
196        )
197
198    # Remote end thinks better of that, and removes the similarly-named stream
199    # it tried to add.
200    node = jp.SetIq(jt2.peer, jt2.jid, [
201        jp.Jingle(jt2.sid, jt2.peer, 'content-remove', [
202            jp.Content(d['name'], 'initiator', d['senders']) ]) ])
203    stream.send(jp.xml(node))
204
205    q.expect_many(
206        EventPattern('stream-iq', iq_type='result', iq_id=node[2]['id']),
207        EventPattern('dbus-signal', signal='StreamRemoved'),
208        )
209
210    # Remote end finally accepts. When Gabble did not namespace contents by
211    # their creator, it would NAK this IQ:
212    #  - Gabble (responder) created a stream called 'foo';
213    #  - test suite (initiator) created a stream called 'foo', which Gabble
214    #    decided would replace its own stream called 'foo';
215    #  - test suite removed its 'foo';
216    #  - test suite accepted Gabble's 'foo', but Gabble didn't believe a stream
217    #    called 'foo' existed any more.
218    node = jp.SetIq(jt2.peer, jt2.jid, [
219        jp.Jingle(jt2.sid, jt2.peer, 'content-accept', [
220            jp.Content(d['name'], d['creator'], d['senders'],
221                jp.Description('video', [
222                    jp.PayloadType(name, str(rate), str(id), parameters) for
223                        (name, id, rate, parameters ) in jt2.audio_codecs ]),
224            jp.TransportGoogleP2P()) ]) ])
225    stream.send(jp.xml(node))
226
227    # We get remote codecs
228    e = q.expect('dbus-signal', signal='SetRemoteCodecs')
229
230    # Now, both we and remote peer try to remove the content simultaneously:
231    # Telepathy client calls RemoveStreams...
232    media_iface.RemoveStreams([id3])
233
234    # ...so Gabble sends a content-remove...
235    e = q.expect('stream-iq', iq_type='set', predicate=lambda x:
236        xpath.queryForNodes("/iq/jingle[@action='content-remove']",
237            x.stanza))
238
239    # ...but before it's acked the peer sends its own content-remove...
240    node = jp.SetIq(jt2.peer, jt2.jid, [
241        jp.Jingle(jt2.sid, jt2.peer, 'content-remove', [
242            jp.Content(c['name'], c['creator'], c['senders']) ]) ])
243    stream.send(jp.xml(node))
244
245    # ...and we don't want Gabble to break when that happens.
246    sync_stream(q, stream)
247
248    # Now we want to remove the first stream
249    media_iface.RemoveStreams([id1])
250
251    # Since this is the last stream, Gabble will just terminate the session.
252    e = q.expect('stream-iq', iq_type='set', predicate=lambda x:
253        xpath.queryForNodes("/iq/jingle[@action='session-terminate']",
254            x.stanza))
255
256if __name__ == '__main__':
257    test_dialects(worker, [JingleProtocol015, JingleProtocol031])
258