1"""
2Test basic roster group functionality.
3"""
4
5from gabbletest import exec_test, acknowledge_iq, sync_stream
6from rostertest import expect_contact_list_signals, check_contact_list_signals
7from servicetest import (assertLength, EventPattern, assertEquals, call_async,
8        sync_dbus, assertContains, assertDoesNotContain)
9import constants as cs
10import ns
11
12from twisted.words.protocols.jabber.client import IQ
13from twisted.words.xish import xpath
14
15def parse_roster_change_request(query, iq):
16    item = query.firstChildElement()
17
18    groups = set()
19
20    for gn in xpath.queryForNodes('/iq/query/item/group', iq):
21        groups.add(str(gn))
22
23    return item['jid'], groups
24
25def send_roster_push(stream, jid, groups):
26    iq = IQ(stream, 'set')
27    query = iq.addElement((ns.ROSTER, 'query'))
28    item = query.addElement('item')
29    item['jid'] = jid
30    item['subscription'] = 'both'
31    for group in groups:
32        item.addElement('group', content=group)
33    stream.send(iq)
34
35def test(q, bus, conn, stream):
36    event = q.expect('stream-iq', query_ns=ns.ROSTER)
37    event.stanza['type'] = 'result'
38
39    item = event.query.addElement('item')
40    item['jid'] = 'amy@foo.com'
41    item['subscription'] = 'both'
42    item.addElement('group', content='women')
43
44    item = event.query.addElement('item')
45    item['jid'] = 'bob@foo.com'
46    item['subscription'] = 'from'
47    item.addElement('group', content='men')
48
49    item = event.query.addElement('item')
50    item['jid'] = 'che@foo.com'
51    item['subscription'] = 'to'
52    item.addElement('group', content='men')
53
54    stream.send(event.stanza)
55
56    # Avoid relying on the implementation detail of exactly when
57    # TpBaseContactList emits ContactsChanged, relative to when it
58    # announces its channels. Prior to 0.20.3, 0.21.1 it would
59    # announce the channels, emit GroupsChanged, then announce the channels
60    # again... which was a bug, but it turned out this test relied on it.
61    #
62    # We do still rely on the implementation detail that we emit GroupsChanged
63    # once per group with all of its members, not once per contact with all
64    # of their groups. On a typical contact list, there are more contacts
65    # than groups, so that'll work out smaller.
66
67    pairs, groups_changed = expect_contact_list_signals(q, bus, conn, [],
68            ['men', 'women'],
69            [
70                EventPattern('dbus-signal', signal='GroupsChanged',
71                    interface=cs.CONN_IFACE_CONTACT_GROUPS,
72                    path=conn.object_path,
73                    predicate=lambda e: 'women' in e.args[1]),
74                EventPattern('dbus-signal', signal='GroupsChanged',
75                    interface=cs.CONN_IFACE_CONTACT_GROUPS,
76                    path=conn.object_path,
77                    predicate=lambda e: 'men' in e.args[1]),
78            ])
79
80    amy, bob, che = conn.RequestHandles(cs.HT_CONTACT,
81            ['amy@foo.com', 'bob@foo.com', 'che@foo.com'])
82
83    assertEquals([[amy], ['women'], []], groups_changed[0].args)
84    assertEquals([[bob, che], ['men'], []], groups_changed[1].args)
85
86    q.expect('dbus-signal', signal='ContactListStateChanged',
87            args=[cs.CONTACT_LIST_STATE_SUCCESS])
88
89    check_contact_list_signals(q, bus, conn, pairs.pop(0), cs.HT_GROUP,
90            'men', ['bob@foo.com', 'che@foo.com'])
91    check_contact_list_signals(q, bus, conn, pairs.pop(0), cs.HT_GROUP,
92            'women', ['amy@foo.com'])
93
94    assertLength(0, pairs)      # i.e. we've checked all of them
95
96    # change Amy's groups
97    call_async(q, conn.ContactGroups, 'SetContactGroups', amy,
98            ['ladies', 'people starting with A'])
99
100    s, iq = q.expect_many(
101        EventPattern('dbus-signal', signal='GroupsCreated'),
102        EventPattern('stream-iq', iq_type='set',
103            query_name='query', query_ns=ns.ROSTER),
104        )
105
106    assertEquals(set(('ladies', 'people starting with A')), set(s.args[0]))
107
108    jid, groups = parse_roster_change_request(iq.query, iq.stanza)
109    assertEquals('amy@foo.com', jid)
110    assertEquals(set(('ladies', 'people starting with A')), groups)
111
112    acknowledge_iq(stream, iq.stanza)
113    q.expect('dbus-return', method='SetContactGroups')
114
115    # Now the server sends us a roster push.
116    send_roster_push(stream, 'amy@foo.com', ['people starting with A', 'ladies'])
117
118    # We get a single signal corresponding to that roster push
119    e = q.expect('dbus-signal', signal='GroupsChanged',
120            predicate=lambda e: e.args[0] == [amy])
121    assertEquals(set(['ladies', 'people starting with A']), set(e.args[1]))
122    assertEquals(['women'], e.args[2])
123
124    # check that Amy's state is what we expected
125    attrs = conn.Contacts.GetContactAttributes([amy],
126            [cs.CONN_IFACE_CONTACT_GROUPS], False)[amy]
127    # make the group list order-independent
128    attrs[cs.CONN_IFACE_CONTACT_GROUPS + '/groups'] = \
129        set(attrs[cs.CONN_IFACE_CONTACT_GROUPS + '/groups'])
130
131    assertEquals({ cs.CONN_IFACE_CONTACT_GROUPS + '/groups':
132                set(['ladies', 'people starting with A']),
133            cs.CONN + '/contact-id': 'amy@foo.com' }, attrs)
134
135    for it_worked in (False, True):
136        # remove a group with a member (the old API couldn't do this)
137        call_async(q, conn.ContactGroups, 'RemoveGroup',
138                'people starting with A')
139
140        iq = q.expect('stream-iq', iq_type='set',
141                query_name='query', query_ns=ns.ROSTER)
142
143        jid, groups = parse_roster_change_request(iq.query, iq.stanza)
144        assertEquals('amy@foo.com', jid)
145        assertEquals(set(('ladies',)), groups)
146
147        acknowledge_iq(stream, iq.stanza)
148
149        # we emit these as soon as the IQ is ack'd, so that we can indicate
150        # group removal...
151        q.expect('dbus-signal', signal='GroupsRemoved',
152                args=[['people starting with A']])
153        q.expect('dbus-signal', signal='GroupsChanged',
154                args=[[amy], [], ['people starting with A']])
155
156        q.expect('dbus-return', method='RemoveGroup')
157
158        if it_worked:
159            # ... although in fact this is what *actually* removes Amy from the
160            # group
161            send_roster_push(stream, 'amy@foo.com', ['ladies'])
162        else:
163            # if the change didn't "stick", this message will revert it
164            send_roster_push(stream, 'amy@foo.com', ['ladies', 'people starting with A'])
165
166            q.expect('dbus-signal', signal='GroupsCreated',
167                    args=[['people starting with A']])
168            q.expect('dbus-signal', signal='GroupsChanged',
169                    args=[[amy], ['people starting with A'], []])
170
171            sync_dbus(bus, q, conn)
172            sync_stream(q, stream)
173            assertEquals({
174                    cs.CONN_IFACE_CONTACT_GROUPS + '/groups':
175                        ['ladies', 'people starting with A'],
176                    cs.CONN + '/contact-id':
177                        'amy@foo.com' },
178                conn.Contacts.GetContactAttributes([amy],
179                    [cs.CONN_IFACE_CONTACT_GROUPS], False)[amy])
180
181    # sanity check: after all that, we expect Amy to be in group 'ladies' only
182    sync_dbus(bus, q, conn)
183    sync_stream(q, stream)
184    assertEquals({ cs.CONN_IFACE_CONTACT_GROUPS + '/groups': ['ladies'],
185            cs.CONN + '/contact-id': 'amy@foo.com' },
186        conn.Contacts.GetContactAttributes([amy],
187            [cs.CONN_IFACE_CONTACT_GROUPS], False)[amy])
188
189    # Rename group 'ladies' to 'girls'
190    call_async(q, conn.ContactGroups, 'RenameGroup', 'ladies', 'girls')
191
192    # Amy is added to 'girls'
193    e = q.expect('stream-iq', iq_type='set', query_name='query', query_ns=ns.ROSTER)
194    jid, groups = parse_roster_change_request(e.query, e.stanza)
195    assertEquals('amy@foo.com', jid)
196    assertEquals(set(['girls', 'ladies']), groups)
197
198    send_roster_push(stream, 'amy@foo.com', ['girls', 'ladies'])
199    acknowledge_iq(stream, e.stanza)
200
201    # Amy is removed from 'ladies'
202    e = q.expect('stream-iq', iq_type='set', query_name='query', query_ns=ns.ROSTER)
203    jid, groups = parse_roster_change_request(e.query, e.stanza)
204    assertEquals('amy@foo.com', jid)
205    assertEquals(set(['girls']), groups)
206
207    send_roster_push(stream, 'amy@foo.com', ['girls'])
208    acknowledge_iq(stream, e.stanza)
209
210    q.expect('dbus-return', method='RenameGroup')
211
212    # check everything has been updated
213    groups = conn.Properties.Get(cs.CONN_IFACE_CONTACT_GROUPS, 'Groups')
214    assertContains('girls', groups)
215    assertDoesNotContain('ladies', groups)
216
217    contacts = conn.ContactList.GetContactListAttributes([cs.CONN_IFACE_CONTACT_GROUPS], False)
218    assertEquals(['girls'], contacts[amy][cs.CONN_IFACE_CONTACT_GROUPS + '/groups'])
219
220if __name__ == '__main__':
221    exec_test(test)
222