def test(q, bus, conn, stream, queued=False):
    event = q.expect('stream-iq', query_ns=ns.ROSTER)
    event.stanza['type'] = 'result'

    item = event.query.addElement('item')
    item['jid'] = '*****@*****.**'
    item['subscription'] = 'none'

    quux_handle = conn.get_contact_handle_sync('*****@*****.**')

    stream.send(event.stanza)

    # slight implementation detail: TpBaseContactList emits ContactsChanged
    # before it announces its channels
    q.expect('dbus-signal', signal='ContactsChangedWithID',
            interface=cs.CONN_IFACE_CONTACT_LIST, path=conn.object_path,
            args=[{quux_handle:
                (cs.SUBSCRIPTION_STATE_NO, cs.SUBSCRIPTION_STATE_NO, '')}, {quux_handle: '*****@*****.**'}, {}])

    check_contact_roster(conn, '*****@*****.**', [], cs.SUBSCRIPTION_STATE_NO, cs.SUBSCRIPTION_STATE_NO)

    if queued:
        conn.Aliasing.SetAliases({quux_handle: 'Quux'})
        set_aliases = q.expect('stream-iq', query_ns=ns.ROSTER)
        item = set_aliases.query.firstChildElement()
        assertEquals('*****@*****.**', item['jid'])
        assertEquals('Quux', item['name'])

    expectations = [
            EventPattern('stream-iq',
            iq_type='set', query_ns=ns.ROSTER),
            ]

    call_async(q, conn.ContactList, 'RemoveContacts', [quux_handle])

    if queued:
        # finish off the previous thing we were doing, so removal can proceed
        acknowledge_iq(stream, set_aliases.stanza)

    event = q.expect_many(*expectations)[0]
    item = event.query.firstChildElement()
    assertEquals('*****@*****.**', item['jid'])
    assertEquals('remove', item['subscription'])

    send_roster_push(stream, '*****@*****.**', 'remove')

    q.expect_many(
            EventPattern('dbus-signal', interface=cs.CONN_IFACE_CONTACT_LIST,
                path=conn.object_path, signal='ContactsChangedWithID',
                args=[{}, {}, {quux_handle: '*****@*****.**'}]),
            EventPattern('stream-iq', iq_id='push', iq_type='result'),
            )

    acknowledge_iq(stream, event.stanza)

    q.expect('dbus-return', method='RemoveContacts')
def check_roster_write(stream, event, jid, name):
    item = event.query.firstChildElement()
    assertEquals(jid, item['jid'])
    # This copes with name=None
    assertEquals(name, item.getAttribute('name'))

    acknowledge_iq(stream, event.stanza)
    # RFC 3921 requires the server to send a roster push to all connected
    # resources whenever a resource updates the roster. Gabble depends on this
    # and pays no attention to its own nick update until the server sends a
    # push.
    send_roster_push(stream, jid, 'none', name=name)
def replying_to_requests(q, bus, conn, stream):
    jid = '*****@*****.**'

    # We shouldn't send receipts to people who aren't on our roster.
    q.forbid_events([EventPattern('stream-message', to=jid)])
    stream.send(
        elem('message', from_=jid, type='chat',
             id='alpha')(elem('body')(u"You didn't kill me, you moron!"),
                         elem(ns.RECEIPTS, 'request')))

    q.expect('dbus-signal', signal='MessageReceived')
    sync_stream(q, stream)
    q.unforbid_all()

    # We should send receipts to people on our roster, seeing as we're not
    # invisible.
    rostertest.send_roster_push(stream, jid, subscription='from')

    stream.send(
        elem('message', from_=jid, type='chat', id='beta')(
            elem('body')(u"You've just destroyed my spiritual essences."),
            elem(ns.RECEIPTS, 'request')))
    q.expect('dbus-signal', signal='MessageReceived')
    e = q.expect('stream-message', to=jid)
    receipt = next(e.stanza.elements(uri=ns.RECEIPTS, name='received'))
    assertEquals('beta', receipt['id'])

    # We would like requests in messages without id=''s not to crash Gabble,
    # and also for it not to send a reply.
    q.forbid_events([EventPattern('stream-message', to=jid)])
    stream.send(
        elem('message', from_=jid, type='chat')(  # NB. no id='' attribute
            elem('body')(u"A favor that I shall now return!"),
            elem(ns.RECEIPTS, 'request')))
    q.expect('dbus-signal', signal='MessageReceived')
    sync_stream(q, stream)
    q.unforbid_all()

    # If we're invisible, LeChuck shouldn't get receipts.
    conn.SimplePresence.SetPresence("hidden", "")
    event = q.expect('stream-iq', query_name='invisible')
    acknowledge_iq(stream, event.stanza)

    q.forbid_events([EventPattern('stream-message', to=jid)])
    stream.send(
        elem('message', from_=jid, type='chat', id='epsilon')(elem('body')(
            u"… but where am I going to find a duck wearing burlap chaps?"),
                                                              elem(
                                                                  ns.RECEIPTS,
                                                                  'request')))
    q.expect('dbus-signal', signal='MessageReceived')
    sync_stream(q, stream)
    q.unforbid_all()
def check_roster_write(stream, event, jid, name):
    item = event.query.firstChildElement()
    assertEquals(jid, item['jid'])
    # This copes with name=None
    assertEquals(name, item.getAttribute('name'))

    acknowledge_iq(stream, event.stanza)
    # RFC 3921 requires the server to send a roster push to all connected
    # resources whenever a resource updates the roster. Gabble depends on this
    # and pays no attention to its own nick update until the server sends a
    # push.
    send_roster_push(stream, jid, 'none', name=name)
def test(q, bus, conn, stream, remove=False):

    call_async(q, conn.ContactList, 'GetContactListAttributes', [], False)
    q.expect('dbus-error', method='GetContactListAttributes',
            name=cs.NOT_YET)

    event = q.expect('stream-iq', query_ns=ns.ROSTER)

    item = event.query.addElement('item')
    item['jid'] = '*****@*****.**'
    item['subscription'] = 'both'
    event.stanza['type'] = 'result'
    stream.send(event.stanza)

    holly, dave, arnold, kristine, cat = conn.get_contact_handles_sync(
            ['*****@*****.**', '*****@*****.**', '*****@*****.**',
                '*****@*****.**', '*****@*****.**'])

    # slight implementation detail: TpBaseContactList emits ContactsChangedWithID
    # before it announces its channels
    s = q.expect('dbus-signal', signal='ContactsChangedWithID',
            interface=cs.CONN_IFACE_CONTACT_LIST, path=conn.object_path)
    assertEquals([{
        holly: (cs.SUBSCRIPTION_STATE_YES, cs.SUBSCRIPTION_STATE_YES, ''),
        }, { holly: '*****@*****.**' }, {}], s.args)

    # this is emitted last, so clients can tell when the initial state dump
    # has finished
    q.expect('dbus-signal', signal='ContactListStateChanged',
            args=[cs.CONTACT_LIST_STATE_SUCCESS])

    call_async(q, conn.ContactList, 'GetContactListAttributes', [], False)
    r = q.expect('dbus-return', method='GetContactListAttributes')
    assertEquals(({
        holly: {
            cs.CONN_IFACE_CONTACT_LIST + '/publish':
                cs.SUBSCRIPTION_STATE_YES,
            cs.CONN_IFACE_CONTACT_LIST + '/subscribe':
                cs.SUBSCRIPTION_STATE_YES,
            cs.CONN + '/contact-id': '*****@*****.**',
            }
        },), r.value)

    # publication authorized for Dave, Holly (the former is pre-authorization,
    # the latter is a no-op)
    call_async(q, conn.ContactList, 'AuthorizePublication', [dave, holly])
    event = q.expect('dbus-return', method='AuthorizePublication')

    # Receive authorization requests from the contacts

    # We pre-authorized Dave, so this is automatically approved
    presence = domish.Element(('jabber:client', 'presence'))
    presence['type'] = 'subscribe'
    presence['from'] = '*****@*****.**'
    stream.send(presence)

    q.expect_many(
            EventPattern('dbus-signal', signal='ContactsChangedWithID',
                args=[{dave: (cs.SUBSCRIPTION_STATE_NO,
                    cs.SUBSCRIPTION_STATE_ASK,
                    '')}, { dave: '*****@*****.**' }, {}]),
            EventPattern('stream-presence', presence_type='subscribed',
                to='*****@*****.**'),
            )

    # Our server responds to Dave being authorized
    send_roster_push(stream, '*****@*****.**', 'from')
    q.expect_many(
            EventPattern('stream-iq', iq_type='result', iq_id='push'),
            EventPattern('dbus-signal', signal='ContactsChangedWithID',
                args=[{dave: (cs.SUBSCRIPTION_STATE_NO,
                    cs.SUBSCRIPTION_STATE_YES, '')}, { dave: '*****@*****.**' }, {}]),
            )

    # The request from Kristine needs authorization (below)
    presence['from'] = '*****@*****.**'
    stream.send(presence)

    q.expect('dbus-signal', signal='ContactsChangedWithID',
            args=[{kristine: (cs.SUBSCRIPTION_STATE_NO,
                cs.SUBSCRIPTION_STATE_ASK, '')}, { kristine: '*****@*****.**' }, {}])

    # This request from Arnold is dealt with below
    presence['from'] = '*****@*****.**'
    stream.send(presence)

    q.expect('dbus-signal', signal='ContactsChangedWithID',
            args=[{arnold: (cs.SUBSCRIPTION_STATE_NO,
                cs.SUBSCRIPTION_STATE_ASK, '')}, { arnold: '*****@*****.**' }, {}])

    returning_method = 'AuthorizePublication'
    call_async(q, conn.ContactList, 'AuthorizePublication',
            [kristine, holly])

    q.expect_many(
            EventPattern('dbus-return', method=returning_method),
            EventPattern('stream-presence', presence_type='subscribed',
                to='*****@*****.**'),
            )

    # Our server acknowledges that we authorized Kristine. Holly's state
    # does not change.
    send_roster_push(stream, '*****@*****.**', 'from')
    q.expect_many(
            EventPattern('dbus-signal', signal='ContactsChangedWithID',
                args=[{kristine: (cs.SUBSCRIPTION_STATE_NO,
                    cs.SUBSCRIPTION_STATE_YES,
                    '')}, { kristine: '*****@*****.**' }, {}]),
            EventPattern('stream-iq', iq_type='result', iq_id='push'),
            )

    # Arnold gives up waiting for us, and cancels his request
    presence['from'] = '*****@*****.**'
    presence['type'] = 'unsubscribe'
    stream.send(presence)

    q.expect_many(
            EventPattern('dbus-signal', signal='ContactsChangedWithID',
                args=[{arnold: (cs.SUBSCRIPTION_STATE_NO,
                    cs.SUBSCRIPTION_STATE_REMOVED_REMOTELY, '')}, { arnold: '*****@*****.**' }, {}]),
            EventPattern('stream-presence', presence_type='unsubscribed',
                to='*****@*****.**'),
            )

    # We can acknowledge that with RemoveContacts or with Unpublish.
    # The old Chan.T.ContactList API can't acknowledge RemovedRemotely,
    # because it sees it as "not there at all" and the group logic drops
    # the "redundant" request.

    if remove:
        returning_method = 'RemoveContacts'
        call_async(q, conn.ContactList, 'RemoveContacts', [arnold])
    else:
        returning_method = 'Unpublish'
        call_async(q, conn.ContactList, 'Unpublish', [arnold])

    # Even if we Unpublish() here, Arnold was never on our XMPP roster,
    # so setting his publish state to SUBSCRIPTION_STATE_NO should result
    # in his removal.
    q.expect_many(
            EventPattern('dbus-return', method=returning_method),
            EventPattern('dbus-signal', signal='ContactsChangedWithID',
                args=[{}, {}, {arnold: '*****@*****.**' }]),
            )

    # Rejecting an authorization request also works
    presence = domish.Element(('jabber:client', 'presence'))
    presence['type'] = 'subscribe'
    presence['from'] = '*****@*****.**'
    stream.send(presence)

    q.expect('dbus-signal', signal='ContactsChangedWithID',
            args=[{cat: (cs.SUBSCRIPTION_STATE_NO,
                cs.SUBSCRIPTION_STATE_ASK,
                '')}, { cat: '*****@*****.**' }, {}])

    if remove:
        returning_method = 'RemoveContacts'
        call_async(q, conn.ContactList, 'RemoveContacts', [cat])
    else:
        returning_method = 'Unpublish'
        call_async(q, conn.ContactList, 'Unpublish', [cat])

    # As above, the only reason the Cat is on our contact list is the pending
    # publish request, so Unpublish really results in removal.
    q.expect_many(
            EventPattern('dbus-return', method=returning_method),
            EventPattern('dbus-signal', signal='ContactsChangedWithID',
                args=[{}, {}, { cat: '*****@*****.**' }]),
            )

    # Redundant API calls (removing an absent contact, etc.) cause no network
    # traffic, and succeed.
    forbidden = [EventPattern('stream-iq', query_ns=ns.ROSTER),
            EventPattern('stream-presence')]
    sync_stream(q, stream)
    sync_dbus(bus, q, conn)
    q.forbid_events(forbidden)

    call_async(q, conn.ContactList, 'AuthorizePublication',
            [kristine, holly, dave])
    call_async(q, conn.ContactList, 'Unpublish', [arnold, cat])
    call_async(q, conn.ContactList, 'RemoveContacts', [arnold, cat])
    q.expect_many(
            EventPattern('dbus-return', method='AuthorizePublication'),
            EventPattern('dbus-return', method='Unpublish'),
            EventPattern('dbus-return', method='RemoveContacts'),
            )

    sync_stream(q, stream)
    sync_dbus(bus, q, conn)
    q.unforbid_events(forbidden)

    # There's one more case: revoking the publish permission of someone who is
    # genuinely on the roster.

    if remove:
        returning_method = 'RemoveContacts'
        call_async(q, conn.ContactList, 'RemoveContacts', [holly])
    else:
        returning_method = 'Unpublish'
        call_async(q, conn.ContactList, 'Unpublish', [holly])

    if remove:
        iq = q.expect('stream-iq', iq_type='set', query_ns=ns.ROSTER,
                query_name='query')

        acknowledge_iq(stream, iq.stanza)

        q.expect('dbus-return', method='RemoveContacts')
        # FIXME: when we depend on a new enough tp-glib, expect RemoveMembers
        # to return here too

        send_roster_push(stream, '*****@*****.**', 'remove')
        q.expect_many(
                EventPattern('stream-iq', iq_type='result', iq_id='push'),
                EventPattern('dbus-signal', signal='ContactsChangedWithID',
                    args=[{}, {}, { holly: '*****@*****.**' }]),
                )
    else:
        q.expect_many(
                EventPattern('dbus-return', method=returning_method),
                EventPattern('stream-presence', presence_type='unsubscribed',
                    to='*****@*****.**'),
                )

        send_roster_push(stream, '*****@*****.**', 'to')
        q.expect_many(
                EventPattern('stream-iq', iq_type='result', iq_id='push'),
                EventPattern('dbus-signal', signal='ContactsChangedWithID',
                    args=[{holly:
                        (cs.SUBSCRIPTION_STATE_YES, cs.SUBSCRIPTION_STATE_NO, ''),
                        }, { holly: '*****@*****.**' }, {}]),
                )
def test(q, bus, conn, stream):
    event, event2 = q.expect_many(
        EventPattern('stream-iq',
                     to=None,
                     query_ns='vcard-temp',
                     query_name='vCard'),
        EventPattern('stream-iq', query_ns=ns.ROSTER))

    acknowledge_iq(stream, event.stanza)
    acknowledge_iq(stream, event2.stanza)

    q.expect('dbus-signal',
             signal='ContactListStateChanged',
             args=[cs.CONTACT_LIST_STATE_SUCCESS])

    # request subscription
    handle = conn.get_contact_handle_sync('*****@*****.**')
    call_async(q, conn.ContactList, 'RequestSubscription', [handle], '')

    event = q.expect('stream-iq', iq_type='set', query_ns=ns.ROSTER)

    acknowledge_iq(stream, event.stanza)
    q.expect('dbus-return', method='RequestSubscription')

    call_async(q, conn.Aliasing, 'RequestAliases', [handle])

    event = q.expect('stream-iq',
                     iq_type='get',
                     query_ns='http://jabber.org/protocol/pubsub',
                     to='*****@*****.**')
    send_pep_nick_reply(stream, event.stanza, 'Bobby')

    event, _ = q.expect_many(
        EventPattern('stream-iq', iq_type='set', query_ns=ns.ROSTER),
        EventPattern('dbus-return',
                     method='RequestAliases',
                     value=(['Bobby'], )))
    check_roster_write(stream, event, '*****@*****.**', 'Bobby')

    # We get a roster push for a contact who for some reason has their alias
    # set on our roster to the empty string (maybe a buggy client?). It's never
    # useful for Gabble to say that someone's alias is the empty string (given
    # the current semantics where the alias is always meant to be something you
    # could show, even if it's just their JID), so let's forbid that.
    jid = '*****@*****.**'
    handle = conn.get_contact_handle_sync(jid)
    q.forbid_events([
        EventPattern('dbus-signal',
                     signal='AliasesChanged',
                     args=[[(handle, '')]])
    ])

    send_roster_push(stream, jid, 'both', name='')
    # I don't really have very strong opinions on whether Gabble should be
    # signalling that this contact's alias has *changed* per se, so am not
    # explicitly expecting that.
    q.expect('dbus-signal', signal='MembersChangedDetailed')

    # But if we ask for it, Gabble should probably send a PEP query.
    h2asv = conn.Contacts.GetContactAttributes([handle],
                                               [cs.CONN_IFACE_ALIASING], False)
    assertEquals(jid, h2asv[handle][cs.ATTR_ALIAS])
    event = q.expect('stream-iq', iq_type='get', query_ns=ns.PUBSUB, to=jid)
    nick = 'Constant Future'

    send_pep_nick_reply(stream, event.stanza, nick)
    expect_AliasesChanged_and_roster_write(q, stream, handle, jid, nick)

    # Here's another contact, whose alias is set on our roster to their JID:
    # because we've cached that they have no alias. Gabble shouldn't make
    # unsolicited PEP or vCard queries to them.
    jid = '*****@*****.**'
    handle = conn.get_contact_handle_sync(jid)

    q.forbid_events([
        EventPattern('stream-iq', query_ns=ns.PUBSUB, to=jid),
        EventPattern('stream-iq', query_ns=ns.VCARD_TEMP, to=jid),
    ])
    send_roster_push(stream, jid, 'both', name=jid)
    q.expect('dbus-signal', signal='AliasesChanged', args=[[(handle, jid)]])
    sync_stream(q, stream)

    # But if we get a PEP nickname update for this contact, Gabble should use
    # the new nickname, and write it back to the roster.
    nick = u'The Friendly Faith Plate'
    stream.send(make_pubsub_event(jid, ns.NICK, elem(ns.NICK, 'nick')(nick)))
    expect_AliasesChanged_and_roster_write(q, stream, handle, jid, nick)

    # As an undocumented extension, we treat setting the alias to the empty
    # string to mean "whatever the contact says their nickname is". (The rest
    # of this test is a regression test for
    # <https://bugs.freedesktop.org/show_bug.cgi?id=11321>.)
    #
    # So first up, let's change the Friendly Faith Plate's nickname to
    # something else.
    custom_nick = u'I saw a deer today'
    conn.Aliasing.SetAliases({handle: custom_nick})
    expect_AliasesChanged_and_roster_write(q, stream, handle, jid, custom_nick)

    assertEquals([custom_nick], conn.Aliasing.RequestAliases([handle]))

    # And now set it to the empty string. Since Gabble happens to have a
    # nickname this contact specified cached, it should switch over to that one.
    conn.Aliasing.SetAliases({handle: ''})
    expect_AliasesChanged_and_roster_write(q, stream, handle, jid, nick)
    assertEquals([nick], conn.Aliasing.RequestAliases([handle]))

    # Here's a contact we haven't seen before, pushed to our roster with a
    # nickname already there.
    jid = '*****@*****.**'
    handle = conn.get_contact_handle_sync(jid)
    nick = 'Potato'

    send_roster_push(stream, jid, 'both', name=nick)
    q.expect('dbus-signal', signal='AliasesChanged', args=[[(handle, nick)]])

    # If the user clears their alias, we should expect Gabble to say over D-Bus
    # that their nickname is their jid, and send a roster push removing the
    # name='' attribute...
    conn.Aliasing.SetAliases({handle: ''})
    expect_AliasesChanged_and_roster_write(q, stream, handle, jid, None)

    # ...and also send a PEP query to find a better nickname; when the contact
    # replies, Gabble should update the roster accordingly.
    event = q.expect('stream-iq', iq_type='get', query_ns=ns.PUBSUB, to=jid)
    send_pep_nick_reply(stream, event.stanza, 'GLaDOS')
    expect_AliasesChanged_and_roster_write(q, stream, handle, jid, 'GLaDOS')
Example #7
0
def replying_to_requests(q, bus, conn, stream):
    jid = '*****@*****.**'

    # We shouldn't send receipts to people who aren't on our roster.
    q.forbid_events([EventPattern('stream-message', to=jid)])
    stream.send(
        elem('message', from_=jid, type='chat', id='alpha')(
          elem('body')(
            u"You didn't kill me, you moron!"
          ),
          elem(ns.RECEIPTS, 'request')
        ))

    q.expect('dbus-signal', signal='MessageReceived')
    sync_stream(q, stream)
    q.unforbid_all()

    # We should send receipts to people on our roster, seeing as we're not
    # invisible.
    rostertest.send_roster_push(stream, jid, subscription='from')

    stream.send(
        elem('message', from_=jid, type='chat', id='beta')(
          elem('body')(
            u"You've just destroyed my spiritual essences."
          ),
          elem(ns.RECEIPTS, 'request')
        ))
    q.expect('dbus-signal', signal='MessageReceived')
    e = q.expect('stream-message', to=jid)
    receipt = e.stanza.elements(uri=ns.RECEIPTS, name='received').next()
    assertEquals('beta', receipt['id'])

    # We would like requests in messages without id=''s not to crash Gabble,
    # and also for it not to send a reply.
    q.forbid_events([EventPattern('stream-message', to=jid)])
    stream.send(
        elem('message', from_=jid, type='chat')( # NB. no id='' attribute
          elem('body')(
            u"A favor that I shall now return!"
          ),
          elem(ns.RECEIPTS, 'request')
        ))
    q.expect('dbus-signal', signal='MessageReceived')
    sync_stream(q, stream)
    q.unforbid_all()

    # If we're invisible, LeChuck shouldn't get receipts.
    conn.SimplePresence.SetPresence("hidden", "")
    event = q.expect('stream-iq', query_name='invisible')
    acknowledge_iq(stream, event.stanza)

    q.forbid_events([EventPattern('stream-message', to=jid)])
    stream.send(
        elem('message', from_=jid, type='chat', id='epsilon')(
          elem('body')(
            u"… but where am I going to find a duck wearing burlap chaps?"
          ),
          elem(ns.RECEIPTS, 'request')
        ))
    q.expect('dbus-signal', signal='MessageReceived')
    sync_stream(q, stream)
    q.unforbid_all()
def test(q, bus, conn, stream):
    event, event2 = q.expect_many(
        EventPattern('stream-iq', to=None, query_ns='vcard-temp',
            query_name='vCard'),
        EventPattern('stream-iq', query_ns=ns.ROSTER))

    acknowledge_iq(stream, event.stanza)
    acknowledge_iq(stream, event2.stanza)

    q.expect('dbus-signal', signal='ContactListStateChanged', args=[cs.CONTACT_LIST_STATE_SUCCESS])

    # request subscription
    handle = conn.get_contact_handle_sync('*****@*****.**')
    call_async(q, conn.ContactList, 'RequestSubscription', [handle], '')

    event = q.expect('stream-iq', iq_type='set', query_ns=ns.ROSTER)

    acknowledge_iq(stream, event.stanza)
    q.expect('dbus-return', method='RequestSubscription')

    call_async(q, conn.Aliasing, 'RequestAliases', [handle])

    event = q.expect('stream-iq', iq_type='get',
        query_ns='http://jabber.org/protocol/pubsub',
        to='*****@*****.**')
    send_pep_nick_reply(stream, event.stanza, 'Bobby')

    event, _ = q.expect_many(
        EventPattern('stream-iq', iq_type='set', query_ns=ns.ROSTER),
        EventPattern('dbus-return', method='RequestAliases',
        value=(['Bobby'],)))
    check_roster_write(stream, event, '*****@*****.**', 'Bobby')

    # We get a roster push for a contact who for some reason has their alias
    # set on our roster to the empty string (maybe a buggy client?). It's never
    # useful for Gabble to say that someone's alias is the empty string (given
    # the current semantics where the alias is always meant to be something you
    # could show, even if it's just their JID), so let's forbid that.
    jid = '*****@*****.**'
    handle = conn.get_contact_handle_sync(jid)
    q.forbid_events([EventPattern('dbus-signal', signal='AliasesChanged',
        args=[[(handle, '')]])])

    send_roster_push(stream, jid, 'both', name='')
    # I don't really have very strong opinions on whether Gabble should be
    # signalling that this contact's alias has *changed* per se, so am not
    # explicitly expecting that.
    q.expect('dbus-signal', signal='MembersChangedDetailed')

    # But if we ask for it, Gabble should probably send a PEP query.
    h2asv = conn.Contacts.GetContactAttributes([handle], [cs.CONN_IFACE_ALIASING], False)
    assertEquals(jid, h2asv[handle][cs.ATTR_ALIAS])
    event = q.expect('stream-iq', iq_type='get', query_ns=ns.PUBSUB, to=jid)
    nick = 'Constant Future'

    send_pep_nick_reply(stream, event.stanza, nick)
    expect_AliasesChanged_and_roster_write(q, stream, handle, jid, nick)

    # Here's another contact, whose alias is set on our roster to their JID:
    # because we've cached that they have no alias. Gabble shouldn't make
    # unsolicited PEP or vCard queries to them.
    jid = '*****@*****.**'
    handle = conn.get_contact_handle_sync(jid)

    q.forbid_events([
        EventPattern('stream-iq', query_ns=ns.PUBSUB, to=jid),
        EventPattern('stream-iq', query_ns=ns.VCARD_TEMP, to=jid),
    ])
    send_roster_push(stream, jid, 'both', name=jid)
    q.expect('dbus-signal', signal='AliasesChanged', args=[[(handle, jid)]])
    sync_stream(q, stream)

    # But if we get a PEP nickname update for this contact, Gabble should use
    # the new nickname, and write it back to the roster.
    nick = u'The Friendly Faith Plate'
    stream.send(make_pubsub_event(jid, ns.NICK, elem(ns.NICK, 'nick')(nick)))
    expect_AliasesChanged_and_roster_write(q, stream, handle, jid, nick)

    # As an undocumented extension, we treat setting the alias to the empty
    # string to mean "whatever the contact says their nickname is". (The rest
    # of this test is a regression test for
    # <https://bugs.freedesktop.org/show_bug.cgi?id=11321>.)
    #
    # So first up, let's change the Friendly Faith Plate's nickname to
    # something else.
    custom_nick = u'I saw a deer today'
    conn.Aliasing.SetAliases({handle: custom_nick})
    expect_AliasesChanged_and_roster_write(q, stream, handle, jid, custom_nick)

    assertEquals([custom_nick], conn.Aliasing.RequestAliases([handle]))

    # And now set it to the empty string. Since Gabble happens to have a
    # nickname this contact specified cached, it should switch over to that one.
    conn.Aliasing.SetAliases({handle: ''})
    expect_AliasesChanged_and_roster_write(q, stream, handle, jid, nick)
    assertEquals([nick], conn.Aliasing.RequestAliases([handle]))

    # Here's a contact we haven't seen before, pushed to our roster with a
    # nickname already there.
    jid = '*****@*****.**'
    handle = conn.get_contact_handle_sync(jid)
    nick = 'Potato'

    send_roster_push(stream, jid, 'both', name=nick)
    q.expect('dbus-signal', signal='AliasesChanged', args=[[(handle, nick)]])

    # If the user clears their alias, we should expect Gabble to say over D-Bus
    # that their nickname is their jid, and send a roster push removing the
    # name='' attribute...
    conn.Aliasing.SetAliases({handle: ''})
    expect_AliasesChanged_and_roster_write(q, stream, handle, jid, None)

    # ...and also send a PEP query to find a better nickname; when the contact
    # replies, Gabble should update the roster accordingly.
    event = q.expect('stream-iq', iq_type='get', query_ns=ns.PUBSUB, to=jid)
    send_pep_nick_reply(stream, event.stanza, 'GLaDOS')
    expect_AliasesChanged_and_roster_write(q, stream, handle, jid, 'GLaDOS')
def test(q, bus, conn, stream, remove=False, remote='accept'):
    event = q.expect('stream-iq', query_ns=ns.ROSTER)
    # send back empty roster
    event.stanza['type'] = 'result'
    stream.send(event.stanza)

    q.expect('dbus-signal',
             signal='ContactListStateChanged',
             args=[cs.CONTACT_LIST_STATE_SUCCESS])

    # request subscription
    alice, bob = conn.get_contact_handles_sync(
        ['*****@*****.**', '*****@*****.**'])

    # Repeated subscription requests are *not* idempotent: the second request
    # should nag the contact again.
    for first_time in True, False, False:
        call_async(q, conn.ContactList, 'RequestSubscription', [bob],
                   'plz add kthx')

        if first_time:
            event = q.expect('stream-iq', iq_type='set', query_ns=ns.ROSTER)
            item = event.query.firstChildElement()
            assertEquals('*****@*****.**', item["jid"])
            acknowledge_iq(stream, event.stanza)

        expectations = [
            EventPattern('stream-presence', presence_type='subscribe'),
        ]

        expectations.append(
            EventPattern('dbus-return', method='RequestSubscription'))

        event = q.expect_many(*expectations)[0]
        assertEquals('plz add kthx', event.presence_status)

        if first_time:
            # Our server sends a roster push indicating that yes, we added him
            send_roster_push(stream, '*****@*****.**', 'none')
            q.expect('stream-iq', iq_type='result', iq_id='push')

            # Our server will also send a roster push with the ask=subscribe
            # sub-state, in response to our <presence type=subscribe>.
            # (RFC 3921 §8.2.4)
            send_roster_push(stream, '*****@*****.**', 'none', True)
            q.expect('stream-iq', iq_type='result', iq_id='push')

    if remote == 'reject':
        # Bob rejects our request.
        presence = domish.Element(('jabber:client', 'presence'))
        presence['from'] = '*****@*****.**'
        presence['type'] = 'unsubscribed'
        stream.send(presence)

        q.expect_many(
            EventPattern(
                'dbus-signal',
                signal='MembersChangedDetailed',
                predicate=lambda e: e.args[0] == [] and e.args[1] == [bob] and
                e.args[2] == [] and e.args[3] == [] and e.args[4][
                    'change-reason'] == cs.GC_REASON_PERMISSION_DENIED),
            #EventPattern('stream-presence'),
            EventPattern('dbus-signal',
                         signal='ContactsChangedWithID',
                         args=[{
                             bob: (cs.SUBSCRIPTION_STATE_REMOVED_REMOTELY,
                                   cs.SUBSCRIPTION_STATE_NO, ''),
                         }, {
                             bob: '*****@*****.**'
                         }, {}]),
        )

        send_roster_push(stream, '*****@*****.**', 'to')
        q.expect('stream-iq', iq_type='result', iq_id='push')
    else:
        # Bob accepts
        presence = domish.Element(('jabber:client', 'presence'))
        presence['from'] = '*****@*****.**'
        presence['type'] = 'subscribed'
        stream.send(presence)

        q.expect_many(
            EventPattern('dbus-signal',
                         signal='MembersChangedDetailed',
                         predicate=lambda e: e.args[0] == [bob] and e.args[1]
                         == [] and e.args[2] == [] and e.args[3] == []),
            EventPattern('stream-presence'),
            EventPattern('dbus-signal',
                         signal='ContactsChangedWithID',
                         args=[{
                             bob: (cs.SUBSCRIPTION_STATE_YES,
                                   cs.SUBSCRIPTION_STATE_NO, ''),
                         }, {
                             bob: '*****@*****.**'
                         }, {}]),
        )

        send_roster_push(stream, '*****@*****.**', 'to')
        q.expect('stream-iq', iq_type='result', iq_id='push')

        # Doing the same again is a successful no-op
        forbidden = [
            EventPattern('stream-iq', query_ns=ns.ROSTER),
            EventPattern('stream-presence')
        ]
        sync_stream(q, stream)
        sync_dbus(bus, q, conn)
        q.forbid_events(forbidden)

        call_async(q, conn.ContactList, 'RequestSubscription', [bob], 'moo')
        q.expect('dbus-return', method='RequestSubscription')

        # Alice is not on the list
        call_async(q, conn.ContactList, 'Unsubscribe', [alice])
        q.expect('dbus-return', method='Unsubscribe')
        call_async(q, conn.ContactList, 'RemoveContacts', [alice])
        q.expect('dbus-return', method='RemoveContacts')

        sync_stream(q, stream)
        sync_dbus(bus, q, conn)
        q.unforbid_events(forbidden)

        if remote == 'revoke':
            # After accepting us, Bob then removes us.
            presence = domish.Element(('jabber:client', 'presence'))
            presence['from'] = '*****@*****.**'
            presence['type'] = 'unsubscribed'
            stream.send(presence)

            q.expect_many(
                EventPattern(
                    'dbus-signal',
                    signal='MembersChangedDetailed',
                    predicate=lambda e: e.args[0] == [] and e.args[1] == [bob]
                    and e.args[2] == [] and e.args[3] == [] and e.args[4][
                        'change-reason'] == cs.GC_REASON_PERMISSION_DENIED),
                EventPattern('stream-presence'),
                EventPattern('dbus-signal',
                             signal='ContactsChangedWithID',
                             args=[{
                                 bob: (cs.SUBSCRIPTION_STATE_REMOVED_REMOTELY,
                                       cs.SUBSCRIPTION_STATE_NO, ''),
                             }, {
                                 bob: '*****@*****.**'
                             }, {}]),
            )

        # Else, Bob isn't actually as interesting as we thought. Never mind,
        # we can unsubscribe or remove him (below), with the same APIs we'd
        # use to acknowledge remote removal.

        # (Unsubscribing from pending-subscribe is tested in
        # roster/removed-from-rp-subscribe.py so we don't test it here.)

    if remove:
        returning_method = 'RemoveContacts'
        call_async(q, conn.ContactList, 'RemoveContacts', [bob])
    else:
        returning_method = 'Unsubscribe'
        call_async(q, conn.ContactList, 'Unsubscribe', [bob])

    if remove:
        iq = q.expect('stream-iq',
                      iq_type='set',
                      query_ns=ns.ROSTER,
                      query_name='query')

        acknowledge_iq(stream, iq.stanza)

        q.expect('dbus-return', method='RemoveContacts')
        # FIXME: when we depend on a new enough tp-glib, expect RemoveMembers
        # to return here too

        send_roster_push(stream, '*****@*****.**', 'remove')
        q.expect_many(
            EventPattern('stream-iq', iq_type='result', iq_id='push'),
            EventPattern('dbus-signal',
                         signal='ContactsChangedWithID',
                         args=[{}, {}, {
                             bob: '*****@*****.**'
                         }]),
        )
    else:
        q.expect_many(
            EventPattern('dbus-return', method=returning_method),
            EventPattern('stream-presence',
                         presence_type='unsubscribe',
                         to='*****@*****.**'),
        )

        send_roster_push(stream, '*****@*****.**', 'none')
        q.expect_many(
            EventPattern('stream-iq', iq_type='result', iq_id='push'),
            EventPattern('dbus-signal',
                         signal='ContactsChangedWithID',
                         args=[{
                             bob: (cs.SUBSCRIPTION_STATE_NO,
                                   cs.SUBSCRIPTION_STATE_NO, ''),
                         }, {
                             bob: '*****@*****.**'
                         }, {}]),
        )
Example #10
0
def test(q, bus, conn, stream, queued=False):
    event = q.expect('stream-iq', query_ns=ns.ROSTER)
    event.stanza['type'] = 'result'

    item = event.query.addElement('item')
    item['jid'] = '*****@*****.**'
    item['subscription'] = 'none'

    quux_handle = conn.get_contact_handle_sync('*****@*****.**')

    stream.send(event.stanza)

    # slight implementation detail: TpBaseContactList emits ContactsChanged
    # before it announces its channels
    q.expect('dbus-signal',
             signal='ContactsChangedWithID',
             interface=cs.CONN_IFACE_CONTACT_LIST,
             path=conn.object_path,
             args=[{
                 quux_handle:
                 (cs.SUBSCRIPTION_STATE_NO, cs.SUBSCRIPTION_STATE_NO, '')
             }, {
                 quux_handle: '*****@*****.**'
             }, {}])

    check_contact_roster(conn, '*****@*****.**', [], cs.SUBSCRIPTION_STATE_NO,
                         cs.SUBSCRIPTION_STATE_NO)

    if queued:
        conn.Aliasing.SetAliases({quux_handle: 'Quux'})
        set_aliases = q.expect('stream-iq', query_ns=ns.ROSTER)
        item = set_aliases.query.firstChildElement()
        assertEquals('*****@*****.**', item['jid'])
        assertEquals('Quux', item['name'])

    expectations = [
        EventPattern('stream-iq', iq_type='set', query_ns=ns.ROSTER),
    ]

    call_async(q, conn.ContactList, 'RemoveContacts', [quux_handle])

    if queued:
        # finish off the previous thing we were doing, so removal can proceed
        acknowledge_iq(stream, set_aliases.stanza)

    event = q.expect_many(*expectations)[0]
    item = event.query.firstChildElement()
    assertEquals('*****@*****.**', item['jid'])
    assertEquals('remove', item['subscription'])

    send_roster_push(stream, '*****@*****.**', 'remove')

    q.expect_many(
        EventPattern('dbus-signal',
                     interface=cs.CONN_IFACE_CONTACT_LIST,
                     path=conn.object_path,
                     signal='ContactsChangedWithID',
                     args=[{}, {}, {
                         quux_handle: '*****@*****.**'
                     }]),
        EventPattern('stream-iq', iq_id='push', iq_type='result'),
    )

    acknowledge_iq(stream, event.stanza)

    q.expect('dbus-return', method='RemoveContacts')
def test(q, bus, conn, stream, modern=True, queued=False):
    event = q.expect('stream-iq', query_ns=ns.ROSTER)
    event.stanza['type'] = 'result'

    item = event.query.addElement('item')
    item['jid'] = '*****@*****.**'
    item['subscription'] = 'none'

    quux_handle = conn.RequestHandles(cs.HT_CONTACT, ['*****@*****.**'])[0]

    stream.send(event.stanza)

    # slight implementation detail: TpBaseContactList emits ContactsChanged
    # before it announces its channels
    q.expect('dbus-signal', signal='ContactsChanged',
            interface=cs.CONN_IFACE_CONTACT_LIST, path=conn.object_path,
            args=[{quux_handle:
                (cs.SUBSCRIPTION_STATE_NO, cs.SUBSCRIPTION_STATE_NO, '')}, []])

    pairs = expect_contact_list_signals(q, bus, conn,
            ['publish', 'subscribe', 'stored'])

    check_contact_list_signals(q, bus, conn, pairs.pop(0), cs.HT_LIST,
            'publish', [])
    check_contact_list_signals(q, bus, conn, pairs.pop(0), cs.HT_LIST,
            'subscribe', [])
    stored = check_contact_list_signals(q, bus, conn, pairs.pop(0), cs.HT_LIST,
            'stored', ['*****@*****.**'])

    assertLength(0, pairs)      # i.e. we've checked all of them

    if queued:
        conn.Aliasing.SetAliases({quux_handle: 'Quux'})
        set_aliases = q.expect('stream-iq', query_ns=ns.ROSTER)
        item = set_aliases.query.firstChildElement()
        assertEquals('*****@*****.**', item['jid'])
        assertEquals('Quux', item['name'])

    expectations = [
            EventPattern('stream-iq',
            iq_type='set', query_ns=ns.ROSTER),
            ]

    if modern:
        call_async(q, conn.ContactList, 'RemoveContacts', [quux_handle])
    else:
        call_async(q, stored.Group, 'RemoveMembers', [quux_handle], '')

    if queued:
        # finish off the previous thing we were doing, so removal can proceed
        acknowledge_iq(stream, set_aliases.stanza)

    event = q.expect_many(*expectations)[0]
    item = event.query.firstChildElement()
    assertEquals('*****@*****.**', item['jid'])
    assertEquals('remove', item['subscription'])

    send_roster_push(stream, '*****@*****.**', 'remove')

    q.expect_many(
            EventPattern('dbus-signal', interface=cs.CHANNEL_IFACE_GROUP,
                path=stored.object_path, signal='MembersChanged',
                args=['', [], [quux_handle], [], [], 0, 0]),
            EventPattern('dbus-signal', interface=cs.CONN_IFACE_CONTACT_LIST,
                path=conn.object_path, signal='ContactsChanged',
                args=[{}, [quux_handle]]),
            EventPattern('stream-iq', iq_id='push', iq_type='result'),
            )

    acknowledge_iq(stream, event.stanza)

    if modern:
        q.expect('dbus-return', method='RemoveContacts')
def test(q, bus, conn, stream, modern=True, remove=False, remote='accept'):
    event = q.expect('stream-iq', query_ns=ns.ROSTER)
    # send back empty roster
    event.stanza['type'] = 'result'
    stream.send(event.stanza)

    while True:
        event = q.expect('dbus-signal', signal='NewChannel')
        path, type, handle_type, handle, suppress_handler = event.args

        if type != cs.CHANNEL_TYPE_CONTACT_LIST:
            continue

        chan_name = conn.InspectHandles(handle_type, [handle])[0]

        if chan_name == 'subscribe':
            break

    chan = wrap_channel(bus.get_object(conn.bus_name, path), 'ContactList')
    assertLength(0, chan.Group.GetMembers())

    stored_path = conn.Requests.EnsureChannel({
        cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_CONTACT_LIST,
        cs.TARGET_HANDLE_TYPE: cs.HT_LIST,
        cs.TARGET_ID: 'stored',
        })[1]
    stored = wrap_channel(bus.get_object(conn.bus_name, stored_path),
            'ContactList')

    # request subscription
    alice, bob = conn.RequestHandles(cs.HT_CONTACT,
            ['*****@*****.**', '*****@*****.**'])

    # Repeated subscription requests are *not* idempotent: the second request
    # should nag the contact again.
    for first_time in True, False, False:
        if modern:
            call_async(q, conn.ContactList, 'RequestSubscription', [bob],
                    'plz add kthx')
        else:
            call_async(q, chan.Group, 'AddMembers', [bob],
                    'plz add kthx')

        if first_time:
            event = q.expect('stream-iq', iq_type='set', query_ns=ns.ROSTER)
            item = event.query.firstChildElement()
            assertEquals('*****@*****.**', item["jid"])
            acknowledge_iq(stream, event.stanza)

        expectations = [
                EventPattern('stream-presence', presence_type='subscribe'),
                ]

        if modern:
            expectations.append(EventPattern('dbus-return',
                method='RequestSubscription'))

        event = q.expect_many(*expectations)[0]
        assertEquals('plz add kthx', event.presence_status)

        if first_time:
            # Our server sends a roster push indicating that yes, we added him
            send_roster_push(stream, '*****@*****.**', 'none')
            q.expect('stream-iq', iq_type='result', iq_id='push')

            # Our server will also send a roster push with the ask=subscribe
            # sub-state, in response to our <presence type=subscribe>.
            # (RFC 3921 §8.2.4)
            send_roster_push(stream, '*****@*****.**', 'none', True)
            q.expect('stream-iq', iq_type='result', iq_id='push')

    if remote == 'reject':
        # Bob rejects our request.
        presence = domish.Element(('jabber:client', 'presence'))
        presence['from'] = '*****@*****.**'
        presence['type'] = 'unsubscribed'
        stream.send(presence)

        q.expect_many(
                EventPattern('dbus-signal', signal='MembersChanged',
                    args=['', [], [bob], [], [], bob,
                        cs.GC_REASON_PERMISSION_DENIED]),
                #EventPattern('stream-presence'),
                EventPattern('dbus-signal', signal='ContactsChanged',
                    args=[{bob:
                        (cs.SUBSCRIPTION_STATE_REMOVED_REMOTELY,
                            cs.SUBSCRIPTION_STATE_NO, ''),
                        }, []]),
                )

        send_roster_push(stream, '*****@*****.**', 'to')
        q.expect('stream-iq', iq_type='result', iq_id='push')
    else:
        # Bob accepts
        presence = domish.Element(('jabber:client', 'presence'))
        presence['from'] = '*****@*****.**'
        presence['type'] = 'subscribed'
        stream.send(presence)

        q.expect_many(
                EventPattern('dbus-signal', signal='MembersChanged',
                    args=['', [bob], [], [], [], bob, 0]),
                EventPattern('stream-presence'),
                EventPattern('dbus-signal', signal='ContactsChanged',
                    args=[{bob:
                        (cs.SUBSCRIPTION_STATE_YES, cs.SUBSCRIPTION_STATE_NO, ''),
                        }, []]),
                )

        send_roster_push(stream, '*****@*****.**', 'to')
        q.expect('stream-iq', iq_type='result', iq_id='push')

        # Doing the same again is a successful no-op
        forbidden = [EventPattern('stream-iq', query_ns=ns.ROSTER),
                EventPattern('stream-presence')]
        sync_stream(q, stream)
        sync_dbus(bus, q, conn)
        q.forbid_events(forbidden)

        call_async(q, conn.ContactList, 'RequestSubscription', [bob], 'moo')
        q.expect('dbus-return', method='RequestSubscription')

        # Alice is not on the list
        call_async(q, conn.ContactList, 'Unsubscribe', [alice])
        q.expect('dbus-return', method='Unsubscribe')
        call_async(q, conn.ContactList, 'RemoveContacts', [alice])
        q.expect('dbus-return', method='RemoveContacts')

        sync_stream(q, stream)
        sync_dbus(bus, q, conn)
        q.unforbid_events(forbidden)

        if remote == 'revoke':
            # After accepting us, Bob then removes us.
            presence = domish.Element(('jabber:client', 'presence'))
            presence['from'] = '*****@*****.**'
            presence['type'] = 'unsubscribed'
            stream.send(presence)

            q.expect_many(
                    EventPattern('dbus-signal', signal='MembersChanged',
                        args=['', [], [bob], [], [], bob,
                            cs.GC_REASON_PERMISSION_DENIED]),
                    EventPattern('stream-presence'),
                    EventPattern('dbus-signal', signal='ContactsChanged',
                        args=[{bob:
                            (cs.SUBSCRIPTION_STATE_REMOVED_REMOTELY,
                                cs.SUBSCRIPTION_STATE_NO, ''),
                            }, []]),
                    )

        # Else, Bob isn't actually as interesting as we thought. Never mind,
        # we can unsubscribe or remove him (below), with the same APIs we'd
        # use to acknowledge remote removal.

        # (Unsubscribing from pending-subscribe is tested in
        # roster/removed-from-rp-subscribe.py so we don't test it here.)

    # If Bob removed us, we have to use modern APIs from now on, because from
    # the point of view of the old Group interface, removed remotely and
    # removed locally are synonymous.
    if remote in ('reject', 'revoke'):
        modern = True

    if modern:
        if remove:
            returning_method = 'RemoveContacts'
            call_async(q, conn.ContactList, 'RemoveContacts', [bob])
        else:
            returning_method = 'Unsubscribe'
            call_async(q, conn.ContactList, 'Unsubscribe', [bob])
    else:
        returning_method = 'RemoveMembers'

        if remove:
            call_async(q, stored.Group, 'RemoveMembers', [bob], '')
        else:
            call_async(q, chan.Group, 'RemoveMembers', [bob], '')

    if remove:
        iq = q.expect('stream-iq', iq_type='set', query_ns=ns.ROSTER,
                query_name='query')

        acknowledge_iq(stream, iq.stanza)

        if modern:
            q.expect('dbus-return', method='RemoveContacts')
        # FIXME: when we depend on a new enough tp-glib, expect RemoveMembers
        # to return here too

        send_roster_push(stream, '*****@*****.**', 'remove')
        q.expect_many(
                EventPattern('stream-iq', iq_type='result', iq_id='push'),
                EventPattern('dbus-signal', signal='ContactsChanged',
                    args=[{}, [bob]]),
                )
    else:
        q.expect_many(
                EventPattern('dbus-return', method=returning_method),
                EventPattern('stream-presence', presence_type='unsubscribe',
                    to='*****@*****.**'),
                )

        send_roster_push(stream, '*****@*****.**', 'none')
        q.expect_many(
                EventPattern('stream-iq', iq_type='result', iq_id='push'),
                EventPattern('dbus-signal', signal='ContactsChanged',
                    args=[{bob:
                        (cs.SUBSCRIPTION_STATE_NO, cs.SUBSCRIPTION_STATE_NO, ''),
                        }, []]),
                )
def test(q, bus, conn, stream, remove=False, remote='accept'):
    event = q.expect('stream-iq', query_ns=ns.ROSTER)
    # send back empty roster
    event.stanza['type'] = 'result'
    stream.send(event.stanza)

    q.expect('dbus-signal', signal='ContactListStateChanged', args=[cs.CONTACT_LIST_STATE_SUCCESS])

    # request subscription
    alice, bob = conn.get_contact_handles_sync(
            ['*****@*****.**', '*****@*****.**'])

    # Repeated subscription requests are *not* idempotent: the second request
    # should nag the contact again.
    for first_time in True, False, False:
        call_async(q, conn.ContactList, 'RequestSubscription', [bob],
                'plz add kthx')

        if first_time:
            event = q.expect('stream-iq', iq_type='set', query_ns=ns.ROSTER)
            item = event.query.firstChildElement()
            assertEquals('*****@*****.**', item["jid"])
            acknowledge_iq(stream, event.stanza)

        expectations = [
                EventPattern('stream-presence', presence_type='subscribe'),
                ]

        expectations.append(EventPattern('dbus-return',
            method='RequestSubscription'))

        event = q.expect_many(*expectations)[0]
        assertEquals('plz add kthx', event.presence_status)

        if first_time:
            # Our server sends a roster push indicating that yes, we added him
            send_roster_push(stream, '*****@*****.**', 'none')
            q.expect('stream-iq', iq_type='result', iq_id='push')

            # Our server will also send a roster push with the ask=subscribe
            # sub-state, in response to our <presence type=subscribe>.
            # (RFC 3921 §8.2.4)
            send_roster_push(stream, '*****@*****.**', 'none', True)
            q.expect('stream-iq', iq_type='result', iq_id='push')

    if remote == 'reject':
        # Bob rejects our request.
        presence = domish.Element(('jabber:client', 'presence'))
        presence['from'] = '*****@*****.**'
        presence['type'] = 'unsubscribed'
        stream.send(presence)

        q.expect_many(
                EventPattern('dbus-signal', signal='MembersChangedDetailed',
                    predicate=lambda e: e.args[0] == [] and e.args[1] == [bob] and
                        e.args[2] == [] and e.args[3] == [] and
                        e.args[4]['change-reason'] == cs.GC_REASON_PERMISSION_DENIED),
                #EventPattern('stream-presence'),
                EventPattern('dbus-signal', signal='ContactsChangedWithID',
                    args=[{bob:
                        (cs.SUBSCRIPTION_STATE_REMOVED_REMOTELY,
                            cs.SUBSCRIPTION_STATE_NO, ''),
                        }, {bob: '*****@*****.**'}, {}]),
                )

        send_roster_push(stream, '*****@*****.**', 'to')
        q.expect('stream-iq', iq_type='result', iq_id='push')
    else:
        # Bob accepts
        presence = domish.Element(('jabber:client', 'presence'))
        presence['from'] = '*****@*****.**'
        presence['type'] = 'subscribed'
        stream.send(presence)

        q.expect_many(
                EventPattern('dbus-signal', signal='MembersChangedDetailed',
                    predicate=lambda e: e.args[0] == [bob] and e.args[1] == [] and
                        e.args[2] == [] and e.args[3] == []),
                EventPattern('stream-presence'),
                EventPattern('dbus-signal', signal='ContactsChangedWithID',
                    args=[{bob:
                        (cs.SUBSCRIPTION_STATE_YES, cs.SUBSCRIPTION_STATE_NO, ''),
                        }, {bob: '*****@*****.**'}, {}]),
                )

        send_roster_push(stream, '*****@*****.**', 'to')
        q.expect('stream-iq', iq_type='result', iq_id='push')

        # Doing the same again is a successful no-op
        forbidden = [EventPattern('stream-iq', query_ns=ns.ROSTER),
                EventPattern('stream-presence')]
        sync_stream(q, stream)
        sync_dbus(bus, q, conn)
        q.forbid_events(forbidden)

        call_async(q, conn.ContactList, 'RequestSubscription', [bob], 'moo')
        q.expect('dbus-return', method='RequestSubscription')

        # Alice is not on the list
        call_async(q, conn.ContactList, 'Unsubscribe', [alice])
        q.expect('dbus-return', method='Unsubscribe')
        call_async(q, conn.ContactList, 'RemoveContacts', [alice])
        q.expect('dbus-return', method='RemoveContacts')

        sync_stream(q, stream)
        sync_dbus(bus, q, conn)
        q.unforbid_events(forbidden)

        if remote == 'revoke':
            # After accepting us, Bob then removes us.
            presence = domish.Element(('jabber:client', 'presence'))
            presence['from'] = '*****@*****.**'
            presence['type'] = 'unsubscribed'
            stream.send(presence)

            q.expect_many(
                    EventPattern('dbus-signal', signal='MembersChangedDetailed',
                        predicate=lambda e: e.args[0] == [] and e.args[1] == [bob] and
                            e.args[2] == [] and e.args[3] == [] and
                            e.args[4]['change-reason'] == cs.GC_REASON_PERMISSION_DENIED),
                    EventPattern('stream-presence'),
                    EventPattern('dbus-signal', signal='ContactsChangedWithID',
                        args=[{bob:
                            (cs.SUBSCRIPTION_STATE_REMOVED_REMOTELY,
                                cs.SUBSCRIPTION_STATE_NO, ''),
                            }, {bob: '*****@*****.**'}, {}]),
                    )

        # Else, Bob isn't actually as interesting as we thought. Never mind,
        # we can unsubscribe or remove him (below), with the same APIs we'd
        # use to acknowledge remote removal.

        # (Unsubscribing from pending-subscribe is tested in
        # roster/removed-from-rp-subscribe.py so we don't test it here.)

    if remove:
        returning_method = 'RemoveContacts'
        call_async(q, conn.ContactList, 'RemoveContacts', [bob])
    else:
        returning_method = 'Unsubscribe'
        call_async(q, conn.ContactList, 'Unsubscribe', [bob])

    if remove:
        iq = q.expect('stream-iq', iq_type='set', query_ns=ns.ROSTER,
                query_name='query')

        acknowledge_iq(stream, iq.stanza)

        q.expect('dbus-return', method='RemoveContacts')
        # FIXME: when we depend on a new enough tp-glib, expect RemoveMembers
        # to return here too

        send_roster_push(stream, '*****@*****.**', 'remove')
        q.expect_many(
                EventPattern('stream-iq', iq_type='result', iq_id='push'),
                EventPattern('dbus-signal', signal='ContactsChangedWithID',
                    args=[{}, {}, {bob: '*****@*****.**'}]),
                )
    else:
        q.expect_many(
                EventPattern('dbus-return', method=returning_method),
                EventPattern('stream-presence', presence_type='unsubscribe',
                    to='*****@*****.**'),
                )

        send_roster_push(stream, '*****@*****.**', 'none')
        q.expect_many(
                EventPattern('stream-iq', iq_type='result', iq_id='push'),
                EventPattern('dbus-signal', signal='ContactsChangedWithID',
                    args=[{bob:
                        (cs.SUBSCRIPTION_STATE_NO, cs.SUBSCRIPTION_STATE_NO, ''),
                        }, {bob: '*****@*****.**'}, {}]),
                )