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